Skip to content

Add LFM2 and LFM2.5 support and testing#6428

Open
qgallouedec wants to merge 6 commits into
mainfrom
lfm2-support
Open

Add LFM2 and LFM2.5 support and testing#6428
qgallouedec wants to merge 6 commits into
mainfrom
lfm2-support

Conversation

@qgallouedec

@qgallouedec qgallouedec commented Jul 16, 2026

Copy link
Copy Markdown
Member

LFM2 sees roughly 1000 runs per week as per our telemetry, but the architecture/template isn't covered by our test suite at all. This adds both, for the two generations that behave differently:

  • trl-internal-testing/tiny-Lfm2ForCausalLM (from LiquidAI/LFM2-1.2B), landed in transformers 4.54.0, below our floor.
  • trl-internal-testing/tiny-Lfm2ForCausalLM-2.5 (from LiquidAI/LFM2.5-230M), gated on transformers>=5.0.0, which its TokenizersBackend tokenizer requires.

Both are wired into the usual parametrized lists (test_data_utils, test_utils, test_sft_trainer,
test_dpo_trainer, test_chat_template_utils).

Chat templates

The two generations differ in a way worth stating plainly:

  • LFM2 renders tools and the tool role, but never reads message["tool_calls"]: an assistant tool call renders as an empty turn. So it goes in the does-not-support tool-calling list, and gets a lfm2_training.jinja patch since its template has no {% generation %} markers.
  • LFM2.5 does render tool_calls, as <|tool_call_start|>[name(key=value, ...)]<|tool_call_end|>. It needs no training patch (its template already ships generation markers and is prefix-preserving), but it does get an lfm2_2_5_template response template so tool calls can be parsed back. Only the new-style template is provided; the legacy response_schema is on its way out and isn't worth adding for a new family.

One known limitation, inherent to the upstream template: argument values are rendered with Jinja's string filter, so flag=False parses back as the string "False". Strings, numbers, and nested JSON round-trip fine.

⚠️ Incidental finding: chunked LM head is bf16-lossy for tied embeddings

Adding LFM2 to _CHUNKED_LM_HEAD_MODEL_IDS surfaced a pre-existing issue. For models that tie their embeddings, the chunked LM head's grad_hidden reaches lm_head.weight through the embedding, where the chunked path (fp32 accumulation) and the reference (whose .float() backward casts to bf16) round differently. Both end up several percent from the true fp32 gradient.

It is not LFM2-specific, the behaviour splits cleanly on tie_word_embeddings across five architectures (~1e-6 fp32 relative error tied vs ~1e-11 untied). Gemma and Cohere are tied too, and stay inside tolerance only because their gradients are ~40x smaller.

Consequently, test_backward is marked xfail(strict=True) for LFM2 at temperature != 1.0.

Follow-ups?

  • LFM2.5-1.2B-Instruct ships a third template variant (308k downloads/month, floor-loadable) that currently hard-fails get_training_chat_template. Highest-value next step.
  • LFM2-VL: Support LFM2-VL multimodal inputs in GRPO and RLOO #6114 shipped tile-indexed handling in GRPO/RLOO that is only tested against synthetic tensors today.

Note

Low Risk
Changes are additive (templates, parsers, tests, docs) with no changes to core training loops; the chunked-LM-head test tweak only affects test numerics.

Overview
Adds first-class support for Liquid LFM2 and LFM2.5 in TRL’s chat-template stack and CI, including tiny hub fixtures and broad test parametrization (SFT, DPO, data utils, template utilities).

LFM2 gets bundled reference templates (lfm2.jinja, lfm2_training.jinja) and automatic swapping via get_training_chat_template for assistant-only loss. Its template renders tools/tool responses but ignores structured tool_calls, so it is classified as not supporting standard tool calling. LFM2.5 ships lfm2_2_5.jinja (already has {% generation %} markers) plus a new lfm2_2_5_template response parser for Python-style <|tool_call_start|>[fn(a=1)]<|tool_call_end|> blocks, thinking fields, and multi-call turns; add_response_schema wires this only as new-style response_template (transformers ≥ 5.13).

Docs and trl/chat_templates/README.md document both families. test_utils.test_backward for chunked LM head now runs in float32 with tighter tolerance so tied-embedding bf16 noise does not mask real gradient bugs when LFM2 joins the model list.

Reviewed by Cursor Bugbot for commit e6cefec. Bugbot is set up for automated code reviews on this repo. Configure here.

@bot-ci-comment

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

Comment on lines +449 to +487
lfm2_2_5_template = {
# LFM2.5 renders every tool call of a turn into a single `<|tool_call_start|>[...]<|tool_call_end|>` block, as a
# comma-separated list of Python-style calls: `[get_weather(city='Paris', days=3), ping()]`. The `open_pattern`
# therefore matches either the opening bracket or the comma separating two calls, and `close_pattern` matches the
# closing paren plus, for the last call, the trailing `]<|tool_call_end|>`.
#
# Argument values are rendered by the template's `format_arg_value` macro: strings single-quoted, mappings as JSON,
# everything else via Jinja's `string` filter. `string_delims` turns the single-quoted strings back into JSON
# strings so they parse. Note that this round-trip is lossy for Python literals the `string` filter emits but JSON
# cannot express: `flag=False` parses back as the string `"False"`, not `False`. That's inherent to the upstream
# template, not something we can recover here.
#
# The thinking field is named `thinking` (not `reasoning_content` as in other families) to match what the LFM2.5
# template itself reads back off the message.
"defaults": {"role": "assistant"},
"start_anchor": "<|im_start|>assistant\n",
"fields": {
"thinking": {
"open": "<think>",
"close_pattern": r"</think>\s*",
"content": "text",
},
"tool_calls": {
"open_pattern": r"(?:<\|tool_call_start\|>\[|,\s*)(?P<name>\w+)\(",
"close_pattern": r"\)(?:\]<\|tool_call_end\|>)?\s*",
"repeats": True,
"content": "xml-inline",
"content_args": {
"tag_pattern": r"(?P<key>\w+)=(?P<value>'[^']*'|\{.*?\}|[^,)]*)",
"value_parser": {"name": "json", "args": {"string_delims": [["'", "'"]], "allow_non_json": True}},
},
"transform": {"type": "function", "function": {"name": "{name}", "arguments": "{content}"}},
},
"content": {
"close_pattern": r"<\|im_end\|>\s*",
"content": "text",
},
},
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Rocketknight1 for information

@kashif kashif left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

curious if the CI passes

Comment thread tests/test_utils.py Outdated
Comment thread trl/chat_template_utils.py

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8e29c92649

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread trl/chat_template_utils.py Outdated
"repeats": True,
"content": "xml-inline",
"content_args": {
"tag_pattern": r"(?P<key>\w+)=(?P<value>'[^']*'|\{.*?\}|[^,)]*)",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve composite LFM2.5 tool arguments

When an LFM2.5 tool call has an array argument or a nested mapping, this regex stops the value at the first comma or first }. The companion template can render valid calls such as foo(items=[1, 2]) or foo(cfg={"a":{"b":1},"c":2}), but response parsing turns those into truncated strings like "[1" or partial JSON before the tool is executed. This silently corrupts tool arguments for GRPO/response parsing; the value parser needs to consume balanced list/object values rather than splitting on commas/braces.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, reproduced: foo(items=[1, 2]) parsed back as {"items": "[1"}, and foo(cfg={"a": {"b": 1}, "c": 2}) as truncated JSON. The value pattern had no branch for [, and its \{.*?\} branch was non-greedy. Fixed by matching balanced brackets (quote-aware, up to three nesting levels, the regex-fixed-depth cap, noted in the comment). Lists, nested mappings, and lists-of-objects now round-trip.

Not sure if it's the best solution, but it seems to work.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c4b6d94. Configure here.

Comment thread tests/test_utils.py
chunked_grad = model_chunked.lm_head.weight.grad.clone()

torch.testing.assert_close(chunked_grad, ref_grad, atol=5e-2, rtol=5e-2)
torch.testing.assert_close(chunked_grad, ref_grad, atol=1e-3, rtol=1e-3)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing tied-embedding backward xfail

Medium Severity

This PR adds LFM2 models to _CHUNKED_LM_HEAD_MODEL_IDS and tightens test_backward, but the PR notes tied-embedding models should xfail at temperature != 1.0. No xfail or skip on tie_word_embeddings appears, so CI can fail for LFM2 and other tied tiny models at temperature=0.7.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c4b6d94. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants