Instructions to use andreagemelli/LFM2.5-350M-Extract-ML-LoRA with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use andreagemelli/LFM2.5-350M-Extract-ML-LoRA with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-generation", model="andreagemelli/LFM2.5-350M-Extract-ML-LoRA") messages = [ {"role": "user", "content": "Who are you?"}, ] pipe(messages)# Load model directly from transformers import AutoTokenizer, AutoModelForCausalLM tokenizer = AutoTokenizer.from_pretrained("andreagemelli/LFM2.5-350M-Extract-ML-LoRA") model = AutoModelForCausalLM.from_pretrained("andreagemelli/LFM2.5-350M-Extract-ML-LoRA", device_map="auto") messages = [ {"role": "user", "content": "Who are you?"}, ] inputs = tokenizer.apply_chat_template( messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt", ).to(model.device) outputs = model.generate(**inputs, max_new_tokens=40) print(tokenizer.decode(outputs[0][inputs["input_ids"].shape[-1]:])) - PEFT
How to use andreagemelli/LFM2.5-350M-Extract-ML-LoRA with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use andreagemelli/LFM2.5-350M-Extract-ML-LoRA with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "andreagemelli/LFM2.5-350M-Extract-ML-LoRA" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "andreagemelli/LFM2.5-350M-Extract-ML-LoRA", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/andreagemelli/LFM2.5-350M-Extract-ML-LoRA
- SGLang
How to use andreagemelli/LFM2.5-350M-Extract-ML-LoRA with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "andreagemelli/LFM2.5-350M-Extract-ML-LoRA" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "andreagemelli/LFM2.5-350M-Extract-ML-LoRA", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "andreagemelli/LFM2.5-350M-Extract-ML-LoRA" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "andreagemelli/LFM2.5-350M-Extract-ML-LoRA", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }' - Docker Model Runner
How to use andreagemelli/LFM2.5-350M-Extract-ML-LoRA with Docker Model Runner:
docker model run hf.co/andreagemelli/LFM2.5-350M-Extract-ML-LoRA
LFM2.5-350M-Extract-ML-LoRA
A 350M model that fills the JSON schema you hand it, and says what kind of document it is looking at — in eight languages.
LoRA fine-tune of LiquidAI/LFM2.5-350M on
andreagemelli/xfund-docai-xl.
Two tasks share one prompt shape: information extraction (return the keys you were given, values copied
verbatim from the page) and document classification (pick one of twelve classes). Italian, German,
Spanish, French, Portuguese, Chinese, Japanese, English — keys, descriptions and class names all in the
document's own language.
Successor to andreagemelli/LFM2.5-350M-IT-Extract
(Italian, extraction only). It powers Scrivano, an offline
desktop Document AI app.
The adapters are already merged into the weights — load it like any causal LM, peft not required.
Results
Measured on the 785-row val split of xfund-docai-xl, greedy decoding (do_sample=False,
max_new_tokens=1024, bf16). Score is per-document field F1 averaged over documents; an output that is
not valid JSON is scored 0 and kept in the average.
| Model | Avg F1 | Non-parsing outputs |
|---|---|---|
LiquidAI/LFM2.5-350M — base, zero-shot |
0.2640 | 195 / 723 |
andreagemelli/LFM2.5-350M-IT-Extract — previous fine-tune, Italian + extraction only |
0.2229 | 229 / 723 |
| full fine-tune, same data and schedule | 0.7364 | 6 / 785 |
| this model — LoRA | 0.7504 | 3 / 785 |
Per language
F1 (non-parsing outputs / rows)
| base | IT-Extract | full fine-tune | LoRA (this model) | |
|---|---|---|---|---|
| it | 0.2459 (17/111) | 0.3135 (37/111) | 0.7141 (0/111) | 0.7269 (0/111) |
| de | 0.2948 (27/91) | 0.2574 (29/91) | 0.7890 (0/91) | 0.8143 (0/91) |
| es | 0.2850 (21/104) | 0.1508 (42/104) | 0.7402 (0/104) | 0.7424 (0/104) |
| fr | 0.2529 (28/115) | 0.2160 (35/115) | 0.7669 (3/115) | 0.8060 (0/115) |
| pt | 0.2268 (21/103) | 0.1575 (40/103) | 0.6026 (1/103) | 0.5948 (2/103) |
| zh | 0.3049 (36/97) | 0.2341 (23/97) | 0.8763 (0/97) | 0.8815 (0/97) |
| ja | 0.2461 (45/102) | 0.2306 (23/102) | 0.7596 (0/102) | 0.7714 (1/102) |
| en | — | — | 0.6011 (2/62) | 0.6281 (0/62) |
Per task
| base | IT-Extract | full fine-tune | LoRA (this model) | |
|---|---|---|---|---|
| information extraction | 0.2517 (191/385) | 0.3979 (74/385) | 0.7410 (6/436) | 0.7617 (3/436) |
| document classification | 0.2781 (4/338) | 0.0237 (155/338) | 0.7307 (0/349) | 0.7364 (0/349) |
| overall | 0.2640 (195/723) | 0.2229 (229/723) | 0.7364 (6/785) | 0.7504 (3/785) |
Reading the numbers
- LoRA beats the full fine-tune on both tasks and on seven of eight languages, on the same data and the same one-epoch budget. Portuguese is the single regression (−0.008), inside the noise of a 103-document split.
- The base model's low score is mostly a formatting failure: 195 of 723 outputs were not JSON at all. It understands the documents better than 0.26 suggests; it does not obey the output contract.
- The Italian-only predecessor is worse than the base model overall. It collapses on classification — 0.0237, with 155 of 338 outputs unparseable — because it never saw the task and answers it with an extraction-shaped object. It also drops below base in six of seven languages. It is better than base at what it was trained on (extraction 0.3979 vs 0.2517, Italian 0.3135 vs 0.2459), which is what overfitting looks like from the inside.
- This model is the most reliable formatter of the four: 3 non-parsing outputs in 785 (0.4%).
- English is the weakest language (0.6281). It comes from FUNSD — noisy business and laboratory paperwork whose pages rarely state a document type, so English classification leans almost entirely on the synthetic pages.
Caveats
- The schema handed to the model lists exactly the keys the page answers. Scores here are an upper bound on a run where the caller does not already know which fields are present.
- The metric double-penalises a wrong value — it counts as a false positive and a false negative.
- Base and IT-Extract were measured on this same val split before English was added (723 of the current 785 rows). Their per-language numbers are directly comparable; their overall averages are over seven languages, not eight.
- Reproduce with
main.pyfrom the Scrivano repo:uv run main.py --model-id andreagemelli/LFM2.5-350M-Extract-ML-LoRA.
Usage
import json, torch
from transformers import AutoModelForCausalLM, AutoTokenizer
model_id = "andreagemelli/LFM2.5-350M-Extract-ML-LoRA"
device = "cuda" if torch.cuda.is_available() else "cpu"
model = AutoModelForCausalLM.from_pretrained(model_id, dtype=torch.bfloat16).to(device)
tokenizer = AutoTokenizer.from_pretrained(model_id)
schema = {
"cognome": "il cognome della persona",
"nome": "il nome di battesimo della persona",
"data-nascita": "la data di nascita della persona",
}
system = (
"You are an expert document analysis model.\n"
"Task: information extraction\n"
"Return a JSON object with exactly the keys listed below, in the same order. Every value "
"must be copied verbatim from the document. Omit a key whose value is absent.\n\n"
"Schema:\n" + "".join(f"{k}: {v}.\n" for k, v in schema.items())
)
page_text = "COMUNE DI TORINO\nCognome: VALLE\nNome: LUISA\nData di nascita: 22/12/1977\n..."
inputs = tokenizer.apply_chat_template(
[{"role": "system", "content": system}, {"role": "user", "content": page_text}],
add_generation_prompt=True, return_tensors="pt", return_dict=True,
).to(device)
out = model.generate(**inputs, max_new_tokens=1024, do_sample=False)
answer = tokenizer.decode(out[0, inputs["input_ids"].shape[1]:], skip_special_tokens=True).strip()
print(json.loads(answer))
# {"cognome": "VALLE", "nome": "LUISA", "data-nascita": "22/12/1977"}
Prompt shape
Both tasks open the same way and diverge on one line. Write the keys, their descriptions and the class names in the document's language — that is how the model was trained.
Information extraction
You are an expert document analysis model.
Task: information extraction
Return a JSON object with exactly the keys listed below, in the same order. Every value must
be copied verbatim from the document. Omit a key whose value is absent.
Schema:
cognome: il cognome della persona.
nome: il nome di battesimo della persona.
data-nascita: la data di nascita della persona.
→ {"cognome": "VALLE", "nome": "LUISA", "data-nascita": "22/12/1977"}
Document classification
You are an expert document analysis model.
Task: document classification
Assign the document to exactly one of the classes listed below. Answer with a JSON object of
the form {"class": "<class>"}.
Classes:
領収書: 代金が支払われたこと、または物品を受領したことを示す書面.
委任状: 本人が代理人に手続きを委任することを示す書面.
… (all twelve, shuffled)
→ {"class": "委任状"}
The twelve classes and the per-language key vocabulary live in
classes.json and
schemas.json.
Training
LoRA via peft, SFT via trl,
one epoch on 2 519 chat examples, on Colab. Adapters merged into the base weights before upload.
| Base model | LiquidAI/LFM2.5-350M (bf16) |
| Dataset | andreagemelli/xfund-docai-xl — 2 519 train / 785 val rows |
| Rank / alpha / dropout | r=128, lora_alpha=256, lora_dropout=0.1, bias="none" |
| Target modules | GLU w1 w2 w3 · attention q_proj k_proj v_proj out_proj · conv in_proj out_proj |
| Epochs · batch size | 1 · 4 per device |
| LR · schedule | 5e-5 · linear, 100 warmup steps |
| Selection | eval each epoch, load_best_model_at_end=True |
from peft import LoraConfig, TaskType
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
inference_mode=False,
r=128,
lora_alpha=256,
lora_dropout=0.1,
target_modules=["w1", "w2", "w3", "q_proj", "k_proj", "v_proj", "out_proj", "in_proj"],
bias="none",
modules_to_save=None,
)
from trl import SFTConfig
sft_config = SFTConfig(
output_dir="./lfm2-sft-lora",
num_train_epochs=1,
per_device_train_batch_size=4,
learning_rate=5e-5,
lr_scheduler_type="linear",
warmup_steps=100,
logging_steps=10,
save_strategy="epoch",
eval_strategy="epoch",
load_best_model_at_end=True,
)
Limitations
- 350M parameters. Read the output. It is a small model doing a copying task; it will occasionally copy the wrong box, and a right-looking value in the wrong field is not flagged.
- Text in, text out. No vision — you supply the page text, from a PDF text layer or your own OCR. In Scrivano, OCR costs roughly 0.10 F1 against a clean text layer.
- The schema is your job. The model mirrors what it is handed; a vague description or a key the page never answers degrades the result. Keep the schema tight and in the document's language.
- JSON is not guaranteed. 3 of 785 val outputs did not parse. Wrap
json.loadsand have a fallback. - Classification is twelve heuristic classes, read off page titles rather than human-annotated, and the English half of it rests mostly on synthetic pages.
- Single-page documents only; no OCR-noise augmentation on the synthetic pages.
- Portuguese and English are the weakest languages (~0.60); Chinese, German and French the strongest.
Related
| Base model | LiquidAI/LFM2.5-350M |
| Dataset | andreagemelli/xfund-docai-xl |
| Previous model (Italian, extraction only) | andreagemelli/LFM2.5-350M-IT-Extract |
| …its GGUF build | andreagemelli/LFM2.5-350M-IT-Extract-GGUF |
| Previous dataset (Italian, extraction only) | andreagemelli/xfund-kie-it |
| App / evaluation code | andreagemelli/scrivano |
The full fine-tune referenced in the tables (andreagemelli/LFM2.5-350M-Extract-ML) is not published — this
LoRA outscores it.
Licence
CC BY-NC-SA 4.0 — NonCommercial, ShareAlike. Inherited from the training data: XFUND is CC BY-NC-SA 4.0 and FUNSD is non-commercial research use only, so the strictest terms apply. Stacked on top of the base model's LFM Open License v1.0; the NonCommercial term governs.
Citation
@misc{gemelli2026lfm25extractml,
title = {LFM2.5-350M-Extract-ML-LoRA: a tiny multilingual model for document
information extraction and classification},
author = {Gemelli, Andrea},
year = {2026},
howpublished = {\url{https://huggingface.co/andreagemelli/LFM2.5-350M-Extract-ML-LoRA}}
}
@misc{gemelli2026xfunddocaixl,
title = {xfund-docai-xl: eight languages, two tasks, one prompt shape},
author = {Gemelli, Andrea},
year = {2026},
howpublished = {\url{https://huggingface.co/datasets/andreagemelli/xfund-docai-xl}}
}
Please also cite the upstream corpora this model is trained on:
@inproceedings{xu2022xfund,
title = {{XFUND}: A Benchmark Dataset for Multilingual Visually Rich Form Understanding},
author = {Xu, Yiheng and Lv, Tengchao and Cui, Lei and Wang, Guoxin and Lu, Yijuan and
Florencio, Dinei and Zhang, Cha and Wei, Furu},
booktitle = {Findings of the Association for Computational Linguistics: ACL 2022},
year = {2022}
}
@inproceedings{jaume2019funsd,
title = {{FUNSD}: A Dataset for Form Understanding in Noisy Scanned Documents},
author = {Jaume, Guillaume and Kemal Ekenel, Hazim and Thiran, Jean-Philippe},
booktitle = {2019 International Conference on Document Analysis and Recognition Workshops
(ICDARW)},
year = {2019}
}
- Downloads last month
- 209