{
  "@context": "https://schema.org",
  "@id": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-09-03-fine-tuning-a-350m-model-for-better-structured-outputs-in/#webpage",
  "@type": [
    "WebPage",
    "ArchiveComponent"
  ],
  "articleBody": "This guide is a fully public, inexpensive recipe for making a small model substantially better at structured-output compliance. We fine-tune [LFM2.5-350M](https://huggingface.co/LiquidAI/LFM2.5-350M) with Group Relative Policy Optimization (GRPO) using the [TRL library](https://huggingface.co/docs/trl/en/index) and evaluate it on the [IFStruct benchmark](https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0). The full run takes around 500 samples and 100 training steps, small enough for a free-tier Colab or Kaggle GPU, and is available on [GitHub](https://github.com/Liquid4All/cookbook/blob/main/finetuning/notebooks/grpo_with_trl_ifstruct.ipynb). The results show that even a light fine-tuning procedure improves performance **from 22.6% to 29.7%** on the IFStruct benchmark.\n\nStructured output is one of the most common real-world tasks for LLMs, yet most benchmarks fold it into broader reasoning or extraction scores rather than measuring it on its own. Whether a model reliably returns valid, parseable output in the requested format and shape — schema compliance — is often what decides whether it can be wired into a downstream system at all.\n\n*Note that the training pipeline described here is not the one used to train the RL model described in the [IFStruct blog](https://www.liquid.ai/blog/ifstruct-v1.0). This notebook doesn't aim to recreate the IFStruct benchmark score, but to show how task-specific fine-tuning of smaller models can improve performance and match that of far larger models.*\n\n## Prerequisites\n\nThis guide has two halves that run in different places:\n\n- **Fine-tuning** runs on a GPU. The accompanying notebook is sized for a free-tier Colab or Kaggle GPU.\n- **Evaluation** can run locally on a MacBook (here, a MacBook Pro with an Apple M5 Max and 36 GB of unified memory) through `llama.cpp`, which exposes an OpenAI-compatible server that the IFStruct evaluator talks to.\n\nWe will need [`uv`](https://docs.astral.sh/uv/) for the Python tooling and `llama.cpp` for serving. Following the [Liquid AI llama.cpp deployment docs](https://docs.liquid.ai/deployment/on-device/llama-cpp), install `llama.cpp` with Homebrew and verify that `llama-server` is available:\n\n```shell\nbrew install llama.cpp\nllama-server --version\n```\n\n## IFStruct Evaluation on LFM2.5-350M (Base model)\n\nBefore we begin, let's evaluate LFM2.5-350M on the [IFStruct benchmark](https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0) and see whether we can **reproduce the reported score of 21.1%**.\n\n**IFStruct** is a benchmark for testing the validity of LLM outputs and schema adherence. The benchmark is open-source in [Liquid4All/ifstruct](https://github.com/Liquid4All/ifstruct), with the public benchmark dataset available on Hugging Face at [LiquidAI/ifstruct-v1.0](https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0).\n\n```shell\ngit clone https://github.com/Liquid4All/ifstruct.git\n```\n\nFor the eval comparison, we serve the model locally on the MacBook with `llama.cpp`. We will use the `BF16` GGUF ([LiquidAI/LFM2.5-350M-GGUF](https://huggingface.co/LiquidAI/LFM2.5-350M-GGUF)).\n\nThen we start the base-model server with the following command:\n\n```shell\nllama-server \\\n  -hf LiquidAI/LFM2.5-350M-GGUF:BF16 \\\n  -c 32768 \\\n  -np 4 \\\n  -ngl 99 \\\n  --alias LiquidAI/LFM2.5-350M \\\n  --host 127.0.0.1 \\\n  --port 8080\n```\n\n- `--alias`: model name IFStruct sends to the OpenAI-compatible endpoint\n- `-ngl 99`: asks `llama.cpp` to offload all layers to the GPU when available\n- `-np 4`: serves four requests in parallel\n- `-c 32768`: size of the prompt context\n\nOnce the server is running, we can run the full benchmark with 2000 samples:\n\n```shell\nuv run ifstruct-eval \\\n  --model LiquidAI/LFM2.5-350M \\\n  --base-url http://localhost:8080/v1 \\\n  --api-key dummy \\\n  --dataset data/test.jsonl \\\n  --results-file results/lfm2.5-350m-llamacpp-base.json \\\n  --n-threads 4 \\\n  --max-tokens 2048 \\\n  -v\n```\n\n```\n============================================================\nModel: LiquidAI/LFM2.5-350M\n============================================================\nOverall: 452/2000 passed (22.6%)\nAverage latency: 1453ms\n\nBy format:\n  JSON: 180/1000 passed (18.0%)\n  YAML: 272/1000 passed (27.2%)\n\nBy top-level structure:\n  Wrapper key 288/1011 passed (28.5%)\n  Bare list   164/989 passed (16.6%)\n\nBy entity type:\n  test__camera_review                 6/83 passed (7.2%)\n  test__clinical_trial                20/104 passed (19.2%)\n  test__conference_schedule           7/87 passed (8.0%)\n  test__escaping__bug_report_batch    24/89 passed (27.0%)\n  test__escaping__config_snippet_audit 15/85 passed (17.6%)\n  test__escaping__customer_email_thread 5/73 passed (6.8%)\n  test__escaping__dialogue_sample     14/95 passed (14.7%)\n  test__escaping__interview_transcript_segment 21/80 passed (26.2%)\n  test__escaping__log_parser_examples 21/72 passed (29.2%)\n  test__escaping__pr_discussion       22/87 passed (25.3%)\n  test__escaping__repro_steps_batch   16/73 passed (21.9%)\n  test__escaping__screenplay_scene    16/92 passed (17.4%)\n  test__escaping__short_story_chapter 15/84 passed (17.9%)\n  test__escaping__support_ticket_batch 27/73 passed (37.0%)\n  test__escaping__terminal_session_notes 20/70 passed (28.6%)\n  test__event_ticket_booking          49/107 passed (45.8%)\n  test__gpu_review                    6/94 passed (6.4%)\n  test__invoice                       28/86 passed (32.6%)\n  test__job_posting                   25/85 passed (29.4%)\n  test__real_estate_listing           31/82 passed (37.8%)\n  test__recipe                        3/70 passed (4.3%)\n  test__rental_car_booking            27/79 passed (34.2%)\n  test__scientific_experiment         13/69 passed (18.8%)\n  test__travel_itinerary              21/81 passed (25.9%)\n\nCommon errors:\n  7228x required field missing\n  738x wrong item count\n  540x type mismatch\n  317x Unclosed code block\n  190x extraneous field 'notes'\n  181x extraneous field 'path'\n  175x extraneous field 'constraints'\n  170x extraneous field 'type'\n  170x missing code block\n  100x expected bare list, got wrapper\n```\n\nThe [IFStruct release blog reports 21.1% for LFM2.5-350M](https://www.liquid.ai/blog/ifstruct-v1.0). Our local llama.cpp/BF16 setup measures 22.6%, close to the 21.1% reported in the IFStruct blog. We use this local result as the baseline for the same serving stack comparison.\n\n## GRPO Fine-tuning with TRL on Structured Outputs\n\nThe full, runnable pipeline lives in the [accompanying notebook](https://github.com/Liquid4All/cookbook/blob/main/finetuning/notebooks/grpo_with_trl_ifstruct.ipynb). We will cover only the relevant pieces in this section.\n\n### Training data\n\nWe use [`nvidia/Nemotron-RL-instruction_following-structured_outputs`](https://huggingface.co/datasets/nvidia/Nemotron-RL-instruction_following-structured_outputs), which pairs each prompt with a target JSON Schema and an expected field count. We use about 500 samples for training.\n\nBecause the Nemotron data distribution differs from the IFStruct evaluation, we augment the prompts to close two gaps between them:\n\n- **40%** get a \"return the output inside a fenced code block\" instruction appended, so the model learns to *follow* the format instruction rather than always emitting raw JSON.\n- A disjoint **20%** are converted into top-level-array tasks (the schema is wrapped in an `array` with a required item count), which trains bare-list output and item-count compliance.\n\n### Model and LoRA\n\nWe load `LiquidAI/LFM2.5-350M` and attach a LoRA adapter. Because LFM2.5 uses a hybrid attention/convolution architecture, we target the LFM-specific module names:\n\n```py\nlora_config = LoraConfig(\n    r=16, \n    lora_alpha=32, \n    bias=\"none\", \n    task_type=\"CAUSAL_LM\",\n    target_modules=[\n        \"q_proj\", \"k_proj\", \"v_proj\", \"out_proj\", \"in_proj\",\n        \"w1\", \"w2\", \"w3\",\n    ],\n)\n```\n\nThis trains ~6M parameters, about 1.66% of the model.\n\n### Reward functions\n\nThen we define three reward functions, each on a `[0, 1]` scale, which score every completion on whether the extracted *structure* is correct:\n\n- `json_format_reward`: Is the output parseable, and in the requested form? Full credit (`1.0`) for the requested form (fenced vs. raw), `0.2` for the wrong-but-parseable form, `0.0` for unparseable output.\n- `field_count_reward`: Does the object have the expected number of top-level fields? An exact match earns `1.0`, and the score decays linearly with the miss.\n- `schema_validation_reward`: Does the output validate against the row's JSON Schema? It counts every constraint violation and gates partial credit on required-key coverage.\n\nWe combine the three as a weighted sum with `reward_weights=[1.0, 0.5, 2.0]`.\n\n### Training\n\nWe train for 100 steps with 8 generations per prompt group, sized for a free-tier 16 GB GPU:\n\n```py\nfrom trl import GRPOConfig\n\ntraining_args = GRPOConfig(\n    output_dir=\"./outputs/lfm25-350m-nemotron-schema-grpo\",\n    learning_rate=5e-5,\n    max_steps=100,\n    warmup_steps=10,\n    num_generations=8,              # completions sampled per prompt group\n    per_device_train_batch_size=4,\n    gradient_accumulation_steps=8,  # 4 prompt groups per optimizer step\n    steps_per_generation=2,\n    max_completion_length=1024,     # room for nested JSON\n    mask_truncated_completions=False,\n    temperature=1.1,                # hotter sampling keeps groups varied\n    beta=0.01,                      # KL penalty toward the reference model\n    reward_weights=[1.0, 0.5, 2.0], # json_format, field_count, schema_validation\n    logging_steps=1,\n    save_steps=100,\n)\n```\n\nAs you can see in the notebook, over the run, all three reward components climb, the KL from the reference model lifts off zero after warmup, and the truncated-completion fraction stays near zero.\n\n### Merging and saving the model\n\nFinally, we merge the LoRA adapter back into the base weights and save it as a single self-contained checkpoint, ready to convert to GGUF for serving:\n\n```py\nMERGED_DIR = f\"{training_args.output_dir}-merged\"\n\nmerged_model = trainer.model.merge_and_unload()\nmerged_model.save_pretrained(MERGED_DIR)\ntokenizer.save_pretrained(MERGED_DIR)\n```\n\n## IFStruct Evaluation on GRPO Tuned LFM2.5-350M\n\nAfter GRPO fine-tuning, we rerun the IFStruct evaluation. For this, we need to convert the merged model checkpoint into a BF16 GGUF. The converter script ships with the llama.cpp source, so we clone the repo once and install the converter's `gguf` package.\n\n```shell\ngit clone --depth 1 https://github.com/ggml-org/llama.cpp\npip install ./llama.cpp/gguf-py\n\nmkdir -p models\npython llama.cpp/convert_hf_to_gguf.py \\\n  PATH_TO_YOUR_MERGED_MODEL \\\n  --outfile ./models/lfm25-350m-grpo-bf16.gguf \\\n  --outtype bf16\n```\n\nThen we serve the merged model with the following command:\n\n```shell\nllama-server \\\n  -m ./models/lfm25-350m-grpo-bf16.gguf \\\n  --alias lfm25-350m-grpo-structured-output \\\n  -c 32768 \\\n  -np 4 \\\n  -ngl 99 \\\n  --host 127.0.0.1 \\\n  --port 8081\n```\n\nThen, we will run the full IFStruct evaluation again with the fine-tuned model:\n\n```shell\nuv run ifstruct-eval \\\n  --model lfm25-350m-grpo-structured-output \\\n  --base-url http://localhost:8081/v1 \\\n  --api-key dummy \\\n  --dataset data/test.jsonl \\\n  --results-file results/lfm25-350m-grpo.json \\\n  --n-threads 4 \\\n  --max-tokens 2048 \\\n  -v\n```\n\n```\n============================================================\nModel: lfm25-350m-grpo-structured-output\n============================================================\nOverall: 594/2000 passed (29.7%)\nAverage latency: 1518ms\n\nBy format:\n  JSON: 319/1000 passed (31.9%)\n  YAML: 275/1000 passed (27.5%)\n\nBy top-level structure:\n  Wrapper key 300/1011 passed (29.7%)\n  Bare list   294/989 passed (29.7%)\n\nBy entity type:\n  test__camera_review                 5/83 passed (6.0%)\n  test__clinical_trial                31/104 passed (29.8%)\n  test__conference_schedule           11/87 passed (12.6%)\n  test__escaping__bug_report_batch    32/89 passed (36.0%)\n  test__escaping__config_snippet_audit 24/85 passed (28.2%)\n  test__escaping__customer_email_thread 9/73 passed (12.3%)\n  test__escaping__dialogue_sample     17/95 passed (17.9%)\n  test__escaping__interview_transcript_segment 13/80 passed (16.2%)\n  test__escaping__log_parser_examples 33/72 passed (45.8%)\n  test__escaping__pr_discussion       26/87 passed (29.9%)\n  test__escaping__repro_steps_batch   23/73 passed (31.5%)\n  test__escaping__screenplay_scene    34/92 passed (37.0%)\n  test__escaping__short_story_chapter 24/84 passed (28.6%)\n  test__escaping__support_ticket_batch 36/73 passed (49.3%)\n  test__escaping__terminal_session_notes 23/70 passed (32.9%)\n  test__event_ticket_booking          62/107 passed (57.9%)\n  test__gpu_review                    7/94 passed (7.4%)\n  test__invoice                       36/86 passed (41.9%)\n  test__job_posting                   33/85 passed (38.8%)\n  test__real_estate_listing           32/82 passed (39.0%)\n  test__recipe                        7/70 passed (10.0%)\n  test__rental_car_booking            37/79 passed (46.8%)\n  test__scientific_experiment         14/69 passed (20.3%)\n  test__travel_itinerary              25/81 passed (30.9%)\n\nCommon errors:\n  7331x required field missing\n  890x wrong item count\n  555x type mismatch\n  102x expected bare list, got wrapper\n   62x extraneous field 'metadata.tone'\n   55x 6 is greater than maximum 5\n   49x extraneous field 'speaker_labels'\n   47x extraneous field 'tone'\n   44x 'cups' not in allowed values ['mg', 'g', 'kg', 'oz', 'lb', 'ml', 'l', 'cl', 'dl'\n   44x extraneous field 'notes'\n```\n\nComparing the two runs on the identical serving stack:\n\n| IFStruct group | base  | GRPO-tuned | Δ        |\n| -------------- | ----- | ---------- | -------- |\n| **Overall**    | 22.6% | **29.7%**  | **+7.1** |\n| JSON           | 18.0% | 31.9%      | +13.9    |\n| YAML           | 27.2% | 27.5%      | +0.3     |\n| Wrapper key    | 28.5% | 29.7%      | +1.2     |\n| Bare list      | 16.6% | 29.7%      | +13.1    |\n\nThe gains land exactly where the training aimed: the JSON pass rate rises by nearly 14 points (18.0% → 31.9%), while YAML stays mostly the same. While this is still below the [Qwen3.5-2B score of 33.15%](https://www.liquid.ai/blog/ifstruct-v1.0), it shows that even light task-specific fine-tuning can bring a small model close to a larger one.\n\n## Conclusion\n\nA short GRPO run with about 500 samples and 100 steps can lift a small 350M parameter model from 22.6% to 29.7% on IFStruct. The takeaway is that a cheap, task-specific reward signal can make a small model substantially more reliable about *form*, closing much of the gap to models several times its size.\n\nTo reproduce or extend this work, see the original [IFStruct v1.0 blog post](https://www.liquid.ai/blog/ifstruct-v1.0), the [Liquid4All/ifstruct](https://github.com/Liquid4All/ifstruct) benchmark repo, and the [LiquidAI/ifstruct-v1.0](https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0) dataset.\n",
  "authors": [],
  "captured_at": "2026-10-19T01:26:53.998049939+00:00",
  "category": "ai",
  "content_html": "<p>This guide is a fully public, inexpensive recipe for making a small model substantially better at structured-output compliance. We fine-tune <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/LiquidAI/LFM2.5-350M\">LFM2.5-350M</a> with Group Relative Policy Optimization (GRPO) using the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/docs/trl/en/index\">TRL library</a> and evaluate it on the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0\">IFStruct benchmark</a>. The full run takes around 500 samples and 100 training steps, small enough for a free-tier Colab or Kaggle GPU, and is available on <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/Liquid4All/cookbook/blob/main/finetuning/notebooks/grpo_with_trl_ifstruct.ipynb\">GitHub</a>. The results show that even a light fine-tuning procedure improves performance <strong>from 22.6% to 29.7%</strong> on the IFStruct benchmark.</p>\n<p>Structured output is one of the most common real-world tasks for LLMs, yet most benchmarks fold it into broader reasoning or extraction scores rather than measuring it on its own. Whether a model reliably returns valid, parseable output in the requested format and shape — schema compliance — is often what decides whether it can be wired into a downstream system at all.</p>\n<p><em>Note that the training pipeline described here is not the one used to train the RL model described in the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.liquid.ai/blog/ifstruct-v1.0\">IFStruct blog</a>. This notebook doesn't aim to recreate the IFStruct benchmark score, but to show how task-specific fine-tuning of smaller models can improve performance and match that of far larger models.</em></p>\n<h2>Prerequisites</h2>\n<p>This guide has two halves that run in different places:</p>\n<ul>\n<li><strong>Fine-tuning</strong> runs on a GPU. The accompanying notebook is sized for a free-tier Colab or Kaggle GPU.</li>\n<li><strong>Evaluation</strong> can run locally on a MacBook (here, a MacBook Pro with an Apple M5 Max and 36 GB of unified memory) through <code>llama.cpp</code>, which exposes an OpenAI-compatible server that the IFStruct evaluator talks to.</li>\n</ul>\n<p>We will need <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.astral.sh/uv/\"><code>uv</code></a> for the Python tooling and <code>llama.cpp</code> for serving. Following the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://docs.liquid.ai/deployment/on-device/llama-cpp\">Liquid AI llama.cpp deployment docs</a>, install <code>llama.cpp</code> with Homebrew and verify that <code>llama-server</code> is available:</p>\n<pre><code class=\"language-shell\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">brew</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> install llama.cpp</span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">llama-server</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>version</span></span>\n</span></span></code></pre>\n<h2>IFStruct Evaluation on LFM2.5-350M (Base model)</h2>\n<p>Before we begin, let's evaluate LFM2.5-350M on the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0\">IFStruct benchmark</a> and see whether we can <strong>reproduce the reported score of 21.1%</strong>.</p>\n<p><strong>IFStruct</strong> is a benchmark for testing the validity of LLM outputs and schema adherence. The benchmark is open-source in <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/Liquid4All/ifstruct\">Liquid4All/ifstruct</a>, with the public benchmark dataset available on Hugging Face at <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0\">LiquidAI/ifstruct-v1.0</a>.</p>\n<pre><code class=\"language-shell\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">git</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> clone https://github.com/Liquid4All/ifstruct.git</span>\n</span></span></code></pre>\n<p>For the eval comparison, we serve the model locally on the MacBook with <code>llama.cpp</code>. We will use the <code>BF16</code> GGUF (<a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/LiquidAI/LFM2.5-350M-GGUF\">LiquidAI/LFM2.5-350M-GGUF</a>).</p>\n<p>Then we start the base-model server with the following command:</p>\n<pre><code class=\"language-shell\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">llama-server</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  -</span>hf</span> LiquidAI/LFM2.5-350M-GGUF:BF16 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  -</span>c</span> 32768 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  -</span>np</span> 4 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  -</span>ngl</span> 99 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>alias</span> LiquidAI/LFM2.5-350M <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>host</span> 127.0.0.1 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>port</span> 8080</span>\n</span></span></code></pre>\n<ul>\n<li><code>--alias</code>: model name IFStruct sends to the OpenAI-compatible endpoint</li>\n<li><code>-ngl 99</code>: asks <code>llama.cpp</code> to offload all layers to the GPU when available</li>\n<li><code>-np 4</code>: serves four requests in parallel</li>\n<li><code>-c 32768</code>: size of the prompt context</li>\n</ul>\n<p>Once the server is running, we can run the full benchmark with 2000 samples:</p>\n<pre><code class=\"language-shell\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">uv</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> run ifstruct-eval <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>model</span> LiquidAI/LFM2.5-350M <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>base-url</span> http://localhost:8080/v1 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>api-key</span> dummy <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>dataset</span> data/test.jsonl <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>results-file</span> results/lfm2.5-350m-llamacpp-base.json <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>n-threads</span> 4 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>max-tokens</span> 2048 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  -</span>v</span></span>\n</span></span></code></pre>\n<pre><code><span class=\"code-snippet\" data-language=\"YAML\"><span class=\"syntax-source syntax-yaml\"><span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">============================================================</span>\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">Model</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">LiquidAI/LFM2.5-350M</span>\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">============================================================</span>\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">Overall</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">452/2000 passed (22.6%)</span>\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">Average latency</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">1453ms</span>\n\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">By format</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">JSON</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">180/1000 passed (18.0%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">YAML</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">272/1000 passed (27.2%)</span>\n\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">By top-level structure</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">Wrapper key 288/1011 passed (28.5%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">Bare list   164/989 passed (16.6%)</span>\n\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">By entity type</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__camera_review                 6/83 passed (7.2%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__clinical_trial                20/104 passed (19.2%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__conference_schedule           7/87 passed (8.0%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__bug_report_batch    24/89 passed (27.0%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__config_snippet_audit 15/85 passed (17.6%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__customer_email_thread 5/73 passed (6.8%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__dialogue_sample     14/95 passed (14.7%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__interview_transcript_segment 21/80 passed (26.2%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__log_parser_examples 21/72 passed (29.2%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__pr_discussion       22/87 passed (25.3%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__repro_steps_batch   16/73 passed (21.9%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__screenplay_scene    16/92 passed (17.4%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__short_story_chapter 15/84 passed (17.9%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__support_ticket_batch 27/73 passed (37.0%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__terminal_session_notes 20/70 passed (28.6%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__event_ticket_booking          49/107 passed (45.8%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__gpu_review                    6/94 passed (6.4%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__invoice                       28/86 passed (32.6%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__job_posting                   25/85 passed (29.4%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__real_estate_listing           31/82 passed (37.8%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__recipe                        3/70 passed (4.3%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__rental_car_booking            27/79 passed (34.2%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__scientific_experiment         13/69 passed (18.8%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__travel_itinerary              21/81 passed (25.9%)</span>\n\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">Common errors</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">7228x required field missing</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">738x wrong item count</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">540x type mismatch</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">317x Unclosed code block</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">190x extraneous field &#39;notes&#39;</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">181x extraneous field &#39;path&#39;</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">175x extraneous field &#39;constraints&#39;</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">170x extraneous field &#39;type&#39;</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">170x missing code block</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">100x expected bare list, got wrapper</span>\n</span></span></code></pre>\n<p>The <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.liquid.ai/blog/ifstruct-v1.0\">IFStruct release blog reports 21.1% for LFM2.5-350M</a>. Our local llama.cpp/BF16 setup measures 22.6%, close to the 21.1% reported in the IFStruct blog. We use this local result as the baseline for the same serving stack comparison.</p>\n<h2>GRPO Fine-tuning with TRL on Structured Outputs</h2>\n<p>The full, runnable pipeline lives in the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/Liquid4All/cookbook/blob/main/finetuning/notebooks/grpo_with_trl_ifstruct.ipynb\">accompanying notebook</a>. We will cover only the relevant pieces in this section.</p>\n<h3>Training data</h3>\n<p>We use <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/datasets/nvidia/Nemotron-RL-instruction_following-structured_outputs\"><code>nvidia/Nemotron-RL-instruction_following-structured_outputs</code></a>, which pairs each prompt with a target JSON Schema and an expected field count. We use about 500 samples for training.</p>\n<p>Because the Nemotron data distribution differs from the IFStruct evaluation, we augment the prompts to close two gaps between them:</p>\n<ul>\n<li><strong>40%</strong> get a &quot;return the output inside a fenced code block&quot; instruction appended, so the model learns to <em>follow</em> the format instruction rather than always emitting raw JSON.</li>\n<li>A disjoint <strong>20%</strong> are converted into top-level-array tasks (the schema is wrapped in an <code>array</code> with a required item count), which trains bare-list output and item-count compliance.</li>\n</ul>\n<h3>Model and LoRA</h3>\n<p>We load <code>LiquidAI/LFM2.5-350M</code> and attach a LoRA adapter. Because LFM2.5 uses a hybrid attention/convolution architecture, we target the LFM-specific module names:</p>\n<pre><code class=\"language-py\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">lora_config</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">LoraConfig</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\">\n    <span class=\"syntax-variable syntax-parameter syntax-python\">r</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">16</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> \n    <span class=\"syntax-variable syntax-parameter syntax-python\">lora_alpha</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">32</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> \n    <span class=\"syntax-variable syntax-parameter syntax-python\">bias</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">none<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> \n    <span class=\"syntax-variable syntax-parameter syntax-python\">task_type</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">CAUSAL_LM<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n    <span class=\"syntax-variable syntax-parameter syntax-python\">target_modules</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-structure syntax-list syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-list syntax-begin syntax-python\">[</span>\n        <span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">q_proj<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-list syntax-python\">,</span> <span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">k_proj<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-list syntax-python\">,</span> <span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">v_proj<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-list syntax-python\">,</span> <span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">out_proj<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-list syntax-python\">,</span> <span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">in_proj<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-list syntax-python\">,</span>\n        <span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">w1<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-list syntax-python\">,</span> <span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">w2<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-list syntax-python\">,</span> <span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">w3<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-list syntax-python\">,</span>\n    <span class=\"syntax-punctuation syntax-section syntax-list syntax-end syntax-python\">]</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n</span></span></code></pre>\n<p>This trains ~6M parameters, about 1.66% of the model.</p>\n<h3>Reward functions</h3>\n<p>Then we define three reward functions, each on a <code>[0, 1]</code> scale, which score every completion on whether the extracted <em>structure</em> is correct:</p>\n<ul>\n<li><code>json_format_reward</code>: Is the output parseable, and in the requested form? Full credit (<code>1.0</code>) for the requested form (fenced vs. raw), <code>0.2</code> for the wrong-but-parseable form, <code>0.0</code> for unparseable output.</li>\n<li><code>field_count_reward</code>: Does the object have the expected number of top-level fields? An exact match earns <code>1.0</code>, and the score decays linearly with the miss.</li>\n<li><code>schema_validation_reward</code>: Does the output validate against the row's JSON Schema? It counts every constraint violation and gates partial credit on required-key coverage.</li>\n</ul>\n<p>We combine the three as a weighted sum with <code>reward_weights=[1.0, 0.5, 2.0]</code>.</p>\n<h3>Training</h3>\n<p>We train for 100 steps with 8 generations per prompt group, sized for a free-tier 16 GB GPU:</p>\n<pre><code class=\"language-py\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-meta syntax-statement syntax-import syntax-python\"><span class=\"syntax-keyword syntax-control syntax-import syntax-from syntax-python\">from</span></span><span class=\"syntax-meta syntax-statement syntax-import syntax-python\"><span class=\"syntax-meta syntax-import-source syntax-python\"> <span class=\"syntax-meta syntax-import-path syntax-python\"><span class=\"syntax-meta syntax-import-name syntax-python\">trl</span></span> <span class=\"syntax-meta syntax-statement syntax-import syntax-python\"><span class=\"syntax-keyword syntax-control syntax-import syntax-python\">import</span></span></span></span><span class=\"syntax-meta syntax-statement syntax-import syntax-python\"></span><span class=\"syntax-meta syntax-statement syntax-import syntax-python\"> <span class=\"syntax-meta syntax-generic-name syntax-python\">GRPOConfig</span></span>\n\n<span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">training_args</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">GRPOConfig</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\">\n    <span class=\"syntax-variable syntax-parameter syntax-python\">output_dir</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">./outputs/lfm25-350m-nemotron-schema-grpo<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n    <span class=\"syntax-variable syntax-parameter syntax-python\">learning_rate</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-float syntax-python\">5e-5</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n    <span class=\"syntax-variable syntax-parameter syntax-python\">max_steps</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">100</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n    <span class=\"syntax-variable syntax-parameter syntax-python\">warmup_steps</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">10</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n    <span class=\"syntax-variable syntax-parameter syntax-python\">num_generations</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">8</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>              <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> completions sampled per prompt group\n</span>    <span class=\"syntax-variable syntax-parameter syntax-python\">per_device_train_batch_size</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">4</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n    <span class=\"syntax-variable syntax-parameter syntax-python\">gradient_accumulation_steps</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">8</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>  <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> 4 prompt groups per optimizer step\n</span>    <span class=\"syntax-variable syntax-parameter syntax-python\">steps_per_generation</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">2</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n    <span class=\"syntax-variable syntax-parameter syntax-python\">max_completion_length</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">1024</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>     <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> room for nested JSON\n</span>    <span class=\"syntax-variable syntax-parameter syntax-python\">mask_truncated_completions</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-language syntax-python\">False</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n    <span class=\"syntax-variable syntax-parameter syntax-python\">temperature</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-float syntax-python\">1<span class=\"syntax-punctuation syntax-separator syntax-decimal syntax-python\">.</span>1</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>                <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> hotter sampling keeps groups varied\n</span>    <span class=\"syntax-variable syntax-parameter syntax-python\">beta</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-float syntax-python\">0<span class=\"syntax-punctuation syntax-separator syntax-decimal syntax-python\">.</span>01</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>                      <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> KL penalty toward the reference model\n</span>    <span class=\"syntax-variable syntax-parameter syntax-python\">reward_weights</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-meta syntax-structure syntax-list syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-list syntax-begin syntax-python\">[</span><span class=\"syntax-constant syntax-numeric syntax-float syntax-python\">1<span class=\"syntax-punctuation syntax-separator syntax-decimal syntax-python\">.</span>0</span><span class=\"syntax-punctuation syntax-separator syntax-list syntax-python\">,</span> <span class=\"syntax-constant syntax-numeric syntax-float syntax-python\">0<span class=\"syntax-punctuation syntax-separator syntax-decimal syntax-python\">.</span>5</span><span class=\"syntax-punctuation syntax-separator syntax-list syntax-python\">,</span> <span class=\"syntax-constant syntax-numeric syntax-float syntax-python\">2<span class=\"syntax-punctuation syntax-separator syntax-decimal syntax-python\">.</span>0</span><span class=\"syntax-punctuation syntax-section syntax-list syntax-end syntax-python\">]</span></span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span> <span class=\"syntax-comment syntax-line syntax-number-sign syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-comment syntax-python\">#</span> json_format, field_count, schema_validation\n</span>    <span class=\"syntax-variable syntax-parameter syntax-python\">logging_steps</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">1</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n    <span class=\"syntax-variable syntax-parameter syntax-python\">save_steps</span><span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span><span class=\"syntax-constant syntax-numeric syntax-integer syntax-decimal syntax-python\">100</span><span class=\"syntax-punctuation syntax-separator syntax-arguments syntax-python\">,</span>\n</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n</span></span></code></pre>\n<p>As you can see in the notebook, over the run, all three reward components climb, the KL from the reference model lifts off zero after warmup, and the truncated-completion fraction stays near zero.</p>\n<h3>Merging and saving the model</h3>\n<p>Finally, we merge the LoRA adapter back into the base weights and save it as a single self-contained checkpoint, ready to convert to GGUF for serving:</p>\n<pre><code class=\"language-py\"><span class=\"code-snippet\" data-language=\"Python\"><span class=\"syntax-source syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-other syntax-constant syntax-python\">MERGED_DIR</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-storage syntax-type syntax-string syntax-python\">f</span><span class=\"syntax-meta syntax-string syntax-interpolated syntax-python\"><span class=\"syntax-string syntax-quoted syntax-double syntax-python\"><span class=\"syntax-punctuation syntax-definition syntax-string syntax-begin syntax-python\">&quot;</span></span></span><span class=\"syntax-meta syntax-string syntax-interpolated syntax-python\"><span class=\"syntax-meta syntax-interpolation syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-interpolation syntax-begin syntax-python\">{</span><span class=\"syntax-source syntax-python syntax-embedded\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">training_args</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">output_dir</span></span></span></span><span class=\"syntax-meta syntax-interpolation syntax-python\"><span class=\"syntax-punctuation syntax-section syntax-interpolation syntax-end syntax-python\">}</span></span><span class=\"syntax-string syntax-quoted syntax-double syntax-python\">-merged<span class=\"syntax-punctuation syntax-definition syntax-string syntax-end syntax-python\">&quot;</span></span></span>\n\n<span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">merged_model</span></span> <span class=\"syntax-keyword syntax-operator syntax-assignment syntax-python\">=</span> <span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">trainer</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span><span class=\"syntax-meta syntax-generic-name syntax-python\">model</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">merge_and_unload</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n<span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">merged_model</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">save_pretrained</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-other syntax-constant syntax-python\">MERGED_DIR</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n<span class=\"syntax-meta syntax-function-call syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-meta syntax-generic-name syntax-python\">tokenizer</span><span class=\"syntax-punctuation syntax-accessor syntax-dot syntax-python\">.</span></span><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-function syntax-python\">save_pretrained</span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-begin syntax-python\">(</span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-python\"><span class=\"syntax-meta syntax-qualified-name syntax-python\"><span class=\"syntax-variable syntax-other syntax-constant syntax-python\">MERGED_DIR</span></span></span><span class=\"syntax-punctuation syntax-section syntax-arguments syntax-end syntax-python\">)</span></span>\n</span></span></code></pre>\n<h2>IFStruct Evaluation on GRPO Tuned LFM2.5-350M</h2>\n<p>After GRPO fine-tuning, we rerun the IFStruct evaluation. For this, we need to convert the merged model checkpoint into a BF16 GGUF. The converter script ships with the llama.cpp source, so we clone the repo once and install the converter's <code>gguf</code> package.</p>\n<pre><code class=\"language-shell\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">git</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> clone<span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> --</span>depth</span> 1 https://github.com/ggml-org/llama.cpp</span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">pip</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> install ./llama.cpp/gguf-py</span>\n\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">mkdir</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\"> -</span>p</span> models</span>\n<span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">python</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> llama.cpp/convert_hf_to_gguf.py <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span>  PATH_TO_YOUR_MERGED_MODEL <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>outfile</span> ./models/lfm25-350m-grpo-bf16.gguf <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>outtype</span> bf16</span>\n</span></span></code></pre>\n<p>Then we serve the merged model with the following command:</p>\n<pre><code class=\"language-shell\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">llama-server</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  -</span>m</span> ./models/lfm25-350m-grpo-bf16.gguf <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>alias</span> lfm25-350m-grpo-structured-output <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  -</span>c</span> 32768 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  -</span>np</span> 4 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  -</span>ngl</span> 99 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>host</span> 127.0.0.1 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>port</span> 8081</span>\n</span></span></code></pre>\n<p>Then, we will run the full IFStruct evaluation again with the fine-tuned model:</p>\n<pre><code class=\"language-shell\"><span class=\"code-snippet\" data-language=\"Shell\"><span class=\"syntax-source syntax-shell syntax-bash\"><span class=\"syntax-meta syntax-function-call syntax-shell\"><span class=\"syntax-variable syntax-function syntax-shell\">uv</span></span><span class=\"syntax-meta syntax-function-call syntax-arguments syntax-shell\"> run ifstruct-eval <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>model</span> lfm25-350m-grpo-structured-output <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>base-url</span> http://localhost:8081/v1 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>api-key</span> dummy <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>dataset</span> data/test.jsonl <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>results-file</span> results/lfm25-350m-grpo.json <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>n-threads</span> 4 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  --</span>max-tokens</span> 2048 <span class=\"syntax-punctuation syntax-separator syntax-continuation syntax-line syntax-shell\">\\\n</span><span class=\"syntax-variable syntax-parameter syntax-option syntax-shell\"><span class=\"syntax-punctuation syntax-definition syntax-parameter syntax-shell\">  -</span>v</span></span>\n</span></span></code></pre>\n<pre><code><span class=\"code-snippet\" data-language=\"YAML\"><span class=\"syntax-source syntax-yaml\"><span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">============================================================</span>\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">Model</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">lfm25-350m-grpo-structured-output</span>\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">============================================================</span>\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">Overall</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">594/2000 passed (29.7%)</span>\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">Average latency</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">1518ms</span>\n\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">By format</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">JSON</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">319/1000 passed (31.9%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">YAML</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span> <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">275/1000 passed (27.5%)</span>\n\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">By top-level structure</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">Wrapper key 300/1011 passed (29.7%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">Bare list   294/989 passed (29.7%)</span>\n\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">By entity type</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__camera_review                 5/83 passed (6.0%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__clinical_trial                31/104 passed (29.8%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__conference_schedule           11/87 passed (12.6%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__bug_report_batch    32/89 passed (36.0%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__config_snippet_audit 24/85 passed (28.2%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__customer_email_thread 9/73 passed (12.3%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__dialogue_sample     17/95 passed (17.9%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__interview_transcript_segment 13/80 passed (16.2%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__log_parser_examples 33/72 passed (45.8%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__pr_discussion       26/87 passed (29.9%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__repro_steps_batch   23/73 passed (31.5%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__screenplay_scene    34/92 passed (37.0%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__short_story_chapter 24/84 passed (28.6%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__support_ticket_batch 36/73 passed (49.3%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__escaping__terminal_session_notes 23/70 passed (32.9%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__event_ticket_booking          62/107 passed (57.9%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__gpu_review                    7/94 passed (7.4%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__invoice                       36/86 passed (41.9%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__job_posting                   33/85 passed (38.8%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__real_estate_listing           32/82 passed (39.0%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__recipe                        7/70 passed (10.0%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__rental_car_booking            37/79 passed (46.8%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__scientific_experiment         14/69 passed (20.3%)</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">test__travel_itinerary              25/81 passed (30.9%)</span>\n\n<span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\"><span class=\"syntax-entity syntax-name syntax-tag syntax-yaml\">Common errors</span></span><span class=\"syntax-punctuation syntax-separator syntax-key-value syntax-mapping syntax-yaml\">:</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">7331x required field missing</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">890x wrong item count</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">555x type mismatch</span>\n  <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">102x expected bare list, got wrapper</span>\n   <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">62x extraneous field &#39;metadata.tone&#39;</span>\n   <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">55x 6 is greater than maximum 5</span>\n   <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">49x extraneous field &#39;speaker_labels&#39;</span>\n   <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">47x extraneous field &#39;tone&#39;</span>\n   <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">44x &#39;cups&#39; not in allowed values [&#39;mg&#39;, &#39;g&#39;, &#39;kg&#39;, &#39;oz&#39;, &#39;lb&#39;, &#39;ml&#39;, &#39;l&#39;, &#39;cl&#39;, &#39;dl&#39;</span>\n   <span class=\"syntax-string syntax-unquoted syntax-plain syntax-out syntax-yaml\">44x extraneous field &#39;notes&#39;</span>\n</span></span></code></pre>\n<p>Comparing the two runs on the identical serving stack:</p>\n<table>\n<thead>\n<tr>\n<th>IFStruct group</th>\n<th>base</th>\n<th>GRPO-tuned</th>\n<th>Δ</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><strong>Overall</strong></td>\n<td>22.6%</td>\n<td><strong>29.7%</strong></td>\n<td><strong>+7.1</strong></td>\n</tr>\n<tr>\n<td>JSON</td>\n<td>18.0%</td>\n<td>31.9%</td>\n<td>+13.9</td>\n</tr>\n<tr>\n<td>YAML</td>\n<td>27.2%</td>\n<td>27.5%</td>\n<td>+0.3</td>\n</tr>\n<tr>\n<td>Wrapper key</td>\n<td>28.5%</td>\n<td>29.7%</td>\n<td>+1.2</td>\n</tr>\n<tr>\n<td>Bare list</td>\n<td>16.6%</td>\n<td>29.7%</td>\n<td>+13.1</td>\n</tr>\n</tbody>\n</table>\n<p>The gains land exactly where the training aimed: the JSON pass rate rises by nearly 14 points (18.0% → 31.9%), while YAML stays mostly the same. While this is still below the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.liquid.ai/blog/ifstruct-v1.0\">Qwen3.5-2B score of 33.15%</a>, it shows that even light task-specific fine-tuning can bring a small model close to a larger one.</p>\n<h2>Conclusion</h2>\n<p>A short GRPO run with about 500 samples and 100 steps can lift a small 350M parameter model from 22.6% to 29.7% on IFStruct. The takeaway is that a cheap, task-specific reward signal can make a small model substantially more reliable about <em>form</em>, closing much of the gap to models several times its size.</p>\n<p>To reproduce or extend this work, see the original <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://www.liquid.ai/blog/ifstruct-v1.0\">IFStruct v1.0 blog post</a>, the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://github.com/Liquid4All/ifstruct\">Liquid4All/ifstruct</a> benchmark repo, and the <a target=\"_blank\" rel=\"noopener noreferrer\" href=\"https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0\">LiquidAI/ifstruct-v1.0</a> dataset.</p>\n",
  "content_markdown": "This guide is a fully public, inexpensive recipe for making a small model substantially better at structured-output compliance. We fine-tune [LFM2.5-350M](https://huggingface.co/LiquidAI/LFM2.5-350M) with Group Relative Policy Optimization (GRPO) using the [TRL library](https://huggingface.co/docs/trl/en/index) and evaluate it on the [IFStruct benchmark](https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0). The full run takes around 500 samples and 100 training steps, small enough for a free-tier Colab or Kaggle GPU, and is available on [GitHub](https://github.com/Liquid4All/cookbook/blob/main/finetuning/notebooks/grpo_with_trl_ifstruct.ipynb). The results show that even a light fine-tuning procedure improves performance **from 22.6% to 29.7%** on the IFStruct benchmark.\n\nStructured output is one of the most common real-world tasks for LLMs, yet most benchmarks fold it into broader reasoning or extraction scores rather than measuring it on its own. Whether a model reliably returns valid, parseable output in the requested format and shape — schema compliance — is often what decides whether it can be wired into a downstream system at all.\n\n*Note that the training pipeline described here is not the one used to train the RL model described in the [IFStruct blog](https://www.liquid.ai/blog/ifstruct-v1.0). This notebook doesn't aim to recreate the IFStruct benchmark score, but to show how task-specific fine-tuning of smaller models can improve performance and match that of far larger models.*\n\n## Prerequisites\n\nThis guide has two halves that run in different places:\n\n- **Fine-tuning** runs on a GPU. The accompanying notebook is sized for a free-tier Colab or Kaggle GPU.\n- **Evaluation** can run locally on a MacBook (here, a MacBook Pro with an Apple M5 Max and 36 GB of unified memory) through `llama.cpp`, which exposes an OpenAI-compatible server that the IFStruct evaluator talks to.\n\nWe will need [`uv`](https://docs.astral.sh/uv/) for the Python tooling and `llama.cpp` for serving. Following the [Liquid AI llama.cpp deployment docs](https://docs.liquid.ai/deployment/on-device/llama-cpp), install `llama.cpp` with Homebrew and verify that `llama-server` is available:\n\n```shell\nbrew install llama.cpp\nllama-server --version\n```\n\n## IFStruct Evaluation on LFM2.5-350M (Base model)\n\nBefore we begin, let's evaluate LFM2.5-350M on the [IFStruct benchmark](https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0) and see whether we can **reproduce the reported score of 21.1%**.\n\n**IFStruct** is a benchmark for testing the validity of LLM outputs and schema adherence. The benchmark is open-source in [Liquid4All/ifstruct](https://github.com/Liquid4All/ifstruct), with the public benchmark dataset available on Hugging Face at [LiquidAI/ifstruct-v1.0](https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0).\n\n```shell\ngit clone https://github.com/Liquid4All/ifstruct.git\n```\n\nFor the eval comparison, we serve the model locally on the MacBook with `llama.cpp`. We will use the `BF16` GGUF ([LiquidAI/LFM2.5-350M-GGUF](https://huggingface.co/LiquidAI/LFM2.5-350M-GGUF)).\n\nThen we start the base-model server with the following command:\n\n```shell\nllama-server \\\n  -hf LiquidAI/LFM2.5-350M-GGUF:BF16 \\\n  -c 32768 \\\n  -np 4 \\\n  -ngl 99 \\\n  --alias LiquidAI/LFM2.5-350M \\\n  --host 127.0.0.1 \\\n  --port 8080\n```\n\n- `--alias`: model name IFStruct sends to the OpenAI-compatible endpoint\n- `-ngl 99`: asks `llama.cpp` to offload all layers to the GPU when available\n- `-np 4`: serves four requests in parallel\n- `-c 32768`: size of the prompt context\n\nOnce the server is running, we can run the full benchmark with 2000 samples:\n\n```shell\nuv run ifstruct-eval \\\n  --model LiquidAI/LFM2.5-350M \\\n  --base-url http://localhost:8080/v1 \\\n  --api-key dummy \\\n  --dataset data/test.jsonl \\\n  --results-file results/lfm2.5-350m-llamacpp-base.json \\\n  --n-threads 4 \\\n  --max-tokens 2048 \\\n  -v\n```\n\n```\n============================================================\nModel: LiquidAI/LFM2.5-350M\n============================================================\nOverall: 452/2000 passed (22.6%)\nAverage latency: 1453ms\n\nBy format:\n  JSON: 180/1000 passed (18.0%)\n  YAML: 272/1000 passed (27.2%)\n\nBy top-level structure:\n  Wrapper key 288/1011 passed (28.5%)\n  Bare list   164/989 passed (16.6%)\n\nBy entity type:\n  test__camera_review                 6/83 passed (7.2%)\n  test__clinical_trial                20/104 passed (19.2%)\n  test__conference_schedule           7/87 passed (8.0%)\n  test__escaping__bug_report_batch    24/89 passed (27.0%)\n  test__escaping__config_snippet_audit 15/85 passed (17.6%)\n  test__escaping__customer_email_thread 5/73 passed (6.8%)\n  test__escaping__dialogue_sample     14/95 passed (14.7%)\n  test__escaping__interview_transcript_segment 21/80 passed (26.2%)\n  test__escaping__log_parser_examples 21/72 passed (29.2%)\n  test__escaping__pr_discussion       22/87 passed (25.3%)\n  test__escaping__repro_steps_batch   16/73 passed (21.9%)\n  test__escaping__screenplay_scene    16/92 passed (17.4%)\n  test__escaping__short_story_chapter 15/84 passed (17.9%)\n  test__escaping__support_ticket_batch 27/73 passed (37.0%)\n  test__escaping__terminal_session_notes 20/70 passed (28.6%)\n  test__event_ticket_booking          49/107 passed (45.8%)\n  test__gpu_review                    6/94 passed (6.4%)\n  test__invoice                       28/86 passed (32.6%)\n  test__job_posting                   25/85 passed (29.4%)\n  test__real_estate_listing           31/82 passed (37.8%)\n  test__recipe                        3/70 passed (4.3%)\n  test__rental_car_booking            27/79 passed (34.2%)\n  test__scientific_experiment         13/69 passed (18.8%)\n  test__travel_itinerary              21/81 passed (25.9%)\n\nCommon errors:\n  7228x required field missing\n  738x wrong item count\n  540x type mismatch\n  317x Unclosed code block\n  190x extraneous field 'notes'\n  181x extraneous field 'path'\n  175x extraneous field 'constraints'\n  170x extraneous field 'type'\n  170x missing code block\n  100x expected bare list, got wrapper\n```\n\nThe [IFStruct release blog reports 21.1% for LFM2.5-350M](https://www.liquid.ai/blog/ifstruct-v1.0). Our local llama.cpp/BF16 setup measures 22.6%, close to the 21.1% reported in the IFStruct blog. We use this local result as the baseline for the same serving stack comparison.\n\n## GRPO Fine-tuning with TRL on Structured Outputs\n\nThe full, runnable pipeline lives in the [accompanying notebook](https://github.com/Liquid4All/cookbook/blob/main/finetuning/notebooks/grpo_with_trl_ifstruct.ipynb). We will cover only the relevant pieces in this section.\n\n### Training data\n\nWe use [`nvidia/Nemotron-RL-instruction_following-structured_outputs`](https://huggingface.co/datasets/nvidia/Nemotron-RL-instruction_following-structured_outputs), which pairs each prompt with a target JSON Schema and an expected field count. We use about 500 samples for training.\n\nBecause the Nemotron data distribution differs from the IFStruct evaluation, we augment the prompts to close two gaps between them:\n\n- **40%** get a \"return the output inside a fenced code block\" instruction appended, so the model learns to *follow* the format instruction rather than always emitting raw JSON.\n- A disjoint **20%** are converted into top-level-array tasks (the schema is wrapped in an `array` with a required item count), which trains bare-list output and item-count compliance.\n\n### Model and LoRA\n\nWe load `LiquidAI/LFM2.5-350M` and attach a LoRA adapter. Because LFM2.5 uses a hybrid attention/convolution architecture, we target the LFM-specific module names:\n\n```py\nlora_config = LoraConfig(\n    r=16, \n    lora_alpha=32, \n    bias=\"none\", \n    task_type=\"CAUSAL_LM\",\n    target_modules=[\n        \"q_proj\", \"k_proj\", \"v_proj\", \"out_proj\", \"in_proj\",\n        \"w1\", \"w2\", \"w3\",\n    ],\n)\n```\n\nThis trains ~6M parameters, about 1.66% of the model.\n\n### Reward functions\n\nThen we define three reward functions, each on a `[0, 1]` scale, which score every completion on whether the extracted *structure* is correct:\n\n- `json_format_reward`: Is the output parseable, and in the requested form? Full credit (`1.0`) for the requested form (fenced vs. raw), `0.2` for the wrong-but-parseable form, `0.0` for unparseable output.\n- `field_count_reward`: Does the object have the expected number of top-level fields? An exact match earns `1.0`, and the score decays linearly with the miss.\n- `schema_validation_reward`: Does the output validate against the row's JSON Schema? It counts every constraint violation and gates partial credit on required-key coverage.\n\nWe combine the three as a weighted sum with `reward_weights=[1.0, 0.5, 2.0]`.\n\n### Training\n\nWe train for 100 steps with 8 generations per prompt group, sized for a free-tier 16 GB GPU:\n\n```py\nfrom trl import GRPOConfig\n\ntraining_args = GRPOConfig(\n    output_dir=\"./outputs/lfm25-350m-nemotron-schema-grpo\",\n    learning_rate=5e-5,\n    max_steps=100,\n    warmup_steps=10,\n    num_generations=8,              # completions sampled per prompt group\n    per_device_train_batch_size=4,\n    gradient_accumulation_steps=8,  # 4 prompt groups per optimizer step\n    steps_per_generation=2,\n    max_completion_length=1024,     # room for nested JSON\n    mask_truncated_completions=False,\n    temperature=1.1,                # hotter sampling keeps groups varied\n    beta=0.01,                      # KL penalty toward the reference model\n    reward_weights=[1.0, 0.5, 2.0], # json_format, field_count, schema_validation\n    logging_steps=1,\n    save_steps=100,\n)\n```\n\nAs you can see in the notebook, over the run, all three reward components climb, the KL from the reference model lifts off zero after warmup, and the truncated-completion fraction stays near zero.\n\n### Merging and saving the model\n\nFinally, we merge the LoRA adapter back into the base weights and save it as a single self-contained checkpoint, ready to convert to GGUF for serving:\n\n```py\nMERGED_DIR = f\"{training_args.output_dir}-merged\"\n\nmerged_model = trainer.model.merge_and_unload()\nmerged_model.save_pretrained(MERGED_DIR)\ntokenizer.save_pretrained(MERGED_DIR)\n```\n\n## IFStruct Evaluation on GRPO Tuned LFM2.5-350M\n\nAfter GRPO fine-tuning, we rerun the IFStruct evaluation. For this, we need to convert the merged model checkpoint into a BF16 GGUF. The converter script ships with the llama.cpp source, so we clone the repo once and install the converter's `gguf` package.\n\n```shell\ngit clone --depth 1 https://github.com/ggml-org/llama.cpp\npip install ./llama.cpp/gguf-py\n\nmkdir -p models\npython llama.cpp/convert_hf_to_gguf.py \\\n  PATH_TO_YOUR_MERGED_MODEL \\\n  --outfile ./models/lfm25-350m-grpo-bf16.gguf \\\n  --outtype bf16\n```\n\nThen we serve the merged model with the following command:\n\n```shell\nllama-server \\\n  -m ./models/lfm25-350m-grpo-bf16.gguf \\\n  --alias lfm25-350m-grpo-structured-output \\\n  -c 32768 \\\n  -np 4 \\\n  -ngl 99 \\\n  --host 127.0.0.1 \\\n  --port 8081\n```\n\nThen, we will run the full IFStruct evaluation again with the fine-tuned model:\n\n```shell\nuv run ifstruct-eval \\\n  --model lfm25-350m-grpo-structured-output \\\n  --base-url http://localhost:8081/v1 \\\n  --api-key dummy \\\n  --dataset data/test.jsonl \\\n  --results-file results/lfm25-350m-grpo.json \\\n  --n-threads 4 \\\n  --max-tokens 2048 \\\n  -v\n```\n\n```\n============================================================\nModel: lfm25-350m-grpo-structured-output\n============================================================\nOverall: 594/2000 passed (29.7%)\nAverage latency: 1518ms\n\nBy format:\n  JSON: 319/1000 passed (31.9%)\n  YAML: 275/1000 passed (27.5%)\n\nBy top-level structure:\n  Wrapper key 300/1011 passed (29.7%)\n  Bare list   294/989 passed (29.7%)\n\nBy entity type:\n  test__camera_review                 5/83 passed (6.0%)\n  test__clinical_trial                31/104 passed (29.8%)\n  test__conference_schedule           11/87 passed (12.6%)\n  test__escaping__bug_report_batch    32/89 passed (36.0%)\n  test__escaping__config_snippet_audit 24/85 passed (28.2%)\n  test__escaping__customer_email_thread 9/73 passed (12.3%)\n  test__escaping__dialogue_sample     17/95 passed (17.9%)\n  test__escaping__interview_transcript_segment 13/80 passed (16.2%)\n  test__escaping__log_parser_examples 33/72 passed (45.8%)\n  test__escaping__pr_discussion       26/87 passed (29.9%)\n  test__escaping__repro_steps_batch   23/73 passed (31.5%)\n  test__escaping__screenplay_scene    34/92 passed (37.0%)\n  test__escaping__short_story_chapter 24/84 passed (28.6%)\n  test__escaping__support_ticket_batch 36/73 passed (49.3%)\n  test__escaping__terminal_session_notes 23/70 passed (32.9%)\n  test__event_ticket_booking          62/107 passed (57.9%)\n  test__gpu_review                    7/94 passed (7.4%)\n  test__invoice                       36/86 passed (41.9%)\n  test__job_posting                   33/85 passed (38.8%)\n  test__real_estate_listing           32/82 passed (39.0%)\n  test__recipe                        7/70 passed (10.0%)\n  test__rental_car_booking            37/79 passed (46.8%)\n  test__scientific_experiment         14/69 passed (20.3%)\n  test__travel_itinerary              25/81 passed (30.9%)\n\nCommon errors:\n  7331x required field missing\n  890x wrong item count\n  555x type mismatch\n  102x expected bare list, got wrapper\n   62x extraneous field 'metadata.tone'\n   55x 6 is greater than maximum 5\n   49x extraneous field 'speaker_labels'\n   47x extraneous field 'tone'\n   44x 'cups' not in allowed values ['mg', 'g', 'kg', 'oz', 'lb', 'ml', 'l', 'cl', 'dl'\n   44x extraneous field 'notes'\n```\n\nComparing the two runs on the identical serving stack:\n\n| IFStruct group | base  | GRPO-tuned | Δ        |\n| -------------- | ----- | ---------- | -------- |\n| **Overall**    | 22.6% | **29.7%**  | **+7.1** |\n| JSON           | 18.0% | 31.9%      | +13.9    |\n| YAML           | 27.2% | 27.5%      | +0.3     |\n| Wrapper key    | 28.5% | 29.7%      | +1.2     |\n| Bare list      | 16.6% | 29.7%      | +13.1    |\n\nThe gains land exactly where the training aimed: the JSON pass rate rises by nearly 14 points (18.0% → 31.9%), while YAML stays mostly the same. While this is still below the [Qwen3.5-2B score of 33.15%](https://www.liquid.ai/blog/ifstruct-v1.0), it shows that even light task-specific fine-tuning can bring a small model close to a larger one.\n\n## Conclusion\n\nA short GRPO run with about 500 samples and 100 steps can lift a small 350M parameter model from 22.6% to 29.7% on IFStruct. The takeaway is that a cheap, task-specific reward signal can make a small model substantially more reliable about *form*, closing much of the gap to models several times its size.\n\nTo reproduce or extend this work, see the original [IFStruct v1.0 blog post](https://www.liquid.ai/blog/ifstruct-v1.0), the [Liquid4All/ifstruct](https://github.com/Liquid4All/ifstruct) benchmark repo, and the [LiquidAI/ifstruct-v1.0](https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0) dataset.\n",
  "dateCreated": "2026-10-19T01:26:53.998049939+00:00",
  "date_created": "2026-09-03T00:00:00+00:00",
  "date_modified": "2026-09-03T00:00:00+00:00",
  "date_published": "2026-09-03T00:00:00+00:00",
  "description": "This guide is a fully public, inexpensive recipe for making a small model substantially better at structured-output compliance. We fine-tune LFM2.5-350M with Group Relative Policy Optimization (GRPO) using the TRL library and evaluate it…",
  "external_url": "https://huggingface.co/blog/grpo-with-trl-ifstruct",
  "git": {
    "commit": "ebb0941c5c06a2ab2377342d607535262a3e969e",
    "path": "items/huggingface-co/2026/09/2026-09-03-fine-tuning-a-350m-model-for-better-structured-outputs-in.md",
    "permalink": "https://github.com/aymericbeaumet/aggr-instance/blob/ebb0941c5c06a2ab2377342d607535262a3e969e/items/huggingface-co/2026/09/2026-09-03-fine-tuning-a-350m-model-for-better-structured-outputs-in.md"
  },
  "id": "urn:aggr:item:f2b3a710f6484274c8de002544d238d2af41e78c",
  "isBasedOn": {
    "@id": "https://huggingface.co/blog/grpo-with-trl-ifstruct"
  },
  "mainEntity": {
    "@id": "https://huggingface.co/blog/grpo-with-trl-ifstruct",
    "@type": "CreativeWork",
    "archivedAt": {
      "@id": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-09-03-fine-tuning-a-350m-model-for-better-structured-outputs-in/#webpage"
    },
    "dateModified": "2026-09-03T00:00:00+00:00",
    "datePublished": "2026-09-03T00:00:00+00:00",
    "headline": "Fine-tuning a 350M Model for Better Structured Outputs in 100 GRPO Steps",
    "image": {
      "@type": "ImageObject",
      "caption": null,
      "height": 90,
      "url": "https://huggingface.co/blog/assets/grpo-with-trl-ifstruct/thumbnail.png",
      "width": 160
    },
    "timeRequired": "PT8M",
    "url": "https://huggingface.co/blog/grpo-with-trl-ifstruct",
    "wordCount": 1796
  },
  "name": "Archived snapshot: Fine-tuning a 350M Model for Better Structured Outputs in 100 GRPO Steps",
  "preview": {
    "alt": null,
    "color": null,
    "height": 90,
    "placeholder": {
      "data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
      "hash": ""
    },
    "url": "https://huggingface.co/blog/assets/grpo-with-trl-ifstruct/thumbnail.png",
    "width": 160
  },
  "reading_minutes": 8,
  "replicated_at": null,
  "source": "huggingface-co",
  "source_name": "Hugging Face - Blog",
  "summary": null,
  "tags": [],
  "temporalCoverage": "2026-10-19T01:26:53.998049939+00:00",
  "title": "Fine-tuning a 350M Model for Better Structured Outputs in 100 GRPO Steps",
  "url": "https://aggr.aymericbeaumet.com/items/huggingface-co/2026-09-03-fine-tuning-a-350m-model-for-better-structured-outputs-in/",
  "word_count": 1796
}