How UK AISI and EvalEval Are Making Benchmark Results Reproducible

<div class="SVELTE_HYDRATER contents" data-target="RepoCodeCopy" data-props="{}"><div></div><!----></div> <div> <!--[0--><a target="_blank" class="btn mb-5 font-sans text-sm no-underline" href="https://github.com/huggingface/blog/blob/main/evaleval-aisi.md">Update on GitHub</a><!--]--></div><!----> <!----> <!--[-1--><!--]--> <div class="relative overflow-clip [overflow-clip-margin:3rem] [&amp;>:last-child]:mb-0"><!----> <p><!--[--><!--[0--></p><!--]--><!--]-->The <a href="https://evalevalai.com/" rel="nofollow">EvalEval Coalition</a> is thrilled to share that the <a href="https://www.aisi.gov.uk/" rel="nofollow">UK AI Security Institute (AISI)</a> is using EvalEval's infrastructure to openly share evaluation results, supporting more reproducible and verifiable evaluation science.<p></p> <p>AISI and EvalEval have previously collaborated on research that began at a <a href="https://evalevalai.com/events/workshop-2025/" rel="nofollow">joint workshop alongside NeurIPS 2025</a>, and feedback from the Institute has helped shape the <a href="https://evalevalai.com/projects/every-eval-ever/" rel="nofollow">Every Eval Ever (EEE) schema</a>. This next phase of the collaboration puts that shared infrastructure into practice.</p> <h2 class="relative group flex items-baseline"> <a id="why-reproducible-evaluation-reporting-matters" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/evaleval-aisi#why-reproducible-evaluation-reporting-matters" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Why reproducible evaluation reporting matters </span> </h2> <p>As AI deployment accelerates, evaluations are becoming increasingly important sources of evidence about model and system performance. Yet results are reported across many formats, platforms, and outlets, often without enough information to reproduce them. Running the evaluations again may itself be prohibitively expensive.</p> <p>EvalEval's mission is to improve this ecosystem through a shared reporting schema, <a href="https://evalevalai.com/projects/every-eval-ever/" rel="nofollow">Every Eval Ever</a>, and an open platform, <a href="https://evalcards.evalevalai.com/" rel="nofollow">Evaluation Cards</a>, that brings evaluation results and the information needed to interpret them into a common structure.</p> <p>This builds naturally on AISI's work to make evaluation more efficient through <a href="https://arxiv.org/abs/2608.14425" rel="nofollow">OptStop</a>, more statistically rigorous through <a href="https://www.aisi.gov.uk/blog/hibayes-improving-llm-evaluation-with-hierarchical-bayesian-modelling" rel="nofollow">HiBayES</a>, and more standardised in areas including transcript analysis and capability elicitation. Together, AISI and EvalEval are working to diagnose gaps in evaluation reporting and build shared infrastructure to close them.</p> <h2 class="relative group flex items-baseline"> <a id="what-aisi-is-sharing" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/evaleval-aisi#what-aisi-is-sharing" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> What AISI is sharing </span> </h2> <p>Transcript-level transparency matters not only for reproducibility, but also for analysis and diagnosis. In this new phase of the collaboration, AISI is making publicly reported evaluation methods and findings available through Evaluation Cards where appropriate. The release includes verified results, context, and configuration information for the five benchmarks in the paper's main experiment:</p> <ul> <li>HealthBench</li> <li>FrontierMath</li> <li>Humanity's Last Exam</li> <li>SWE-Bench Pro</li> <li>Terminal-Bench 2.0</li> </ul> <p>These results cover six frontier models: Claude Opus 4, Claude Opus 4.5, Claude Opus 4.6, GPT-5, GPT-5.2, and GPT-5.4. The release also includes results from two related cyber evaluations—Cyber CTFs and The Last Ones—which use a different, partially overlapping set of models. The data accompany AISI's paper, <a href="https://arxiv.org/abs/2606.17930" rel="nofollow"><em>How Inference Compute Shapes Frontier LLM Evaluation</em></a>, which studies how benchmark performance depends on inference-time compute and evaluation protocol.</p> <iframe src="https://evaleval-general-eval-card.hf.space/embed/eval/trajectories/aisi-inference-scaling/hle?panel=tokens" frameborder="0" width="100%" height="670px" sandbox="allow-scripts" referrerpolicy="same-origin"></iframe> <p><em>Performance on Humanity's Last Exam changes with evaluation protocol and inference compute. Each curve shows the cumulative share of attempted tasks solved within a given token count, using the earliest observed success per task. When models received correctness feedback from an oracle after each attempt, they continued to solve additional tasks as token use increased.</em></p> <p>When results are openly released with setup information, researchers and practitioners can examine individual studies more closely and compare findings across the wider ecosystem. Where other reports lack these details, releases like AISI's provide verified reference points for interpreting evaluations in context—for example, by helping researchers understand how setup choices may influence reported performance. As more evaluators adopt EEE, open comparisons like these can support broader and more reliable meta-research.</p> <iframe src="https://evaleval-general-eval-card.hf.space/embed/eval/distribution/aisi-inference-scaling/terminal-bench-2?view=context" frameborder="0" width="100%" height="500px" sandbox="allow-scripts" referrerpolicy="same-origin"></iframe> <p><em>AISI's Terminal-Bench 2.0 results alongside other reported evaluations for the same models, under different evaluation setups.</em></p> <p>We are excited about this adoption and look forward to further standardising and sharing evaluations with AISI and other AI evaluation organisations.</p> <h2 class="relative group flex items-baseline"> <a id="contribute-to-the-shared-mission" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/evaleval-aisi#contribute-to-the-shared-mission" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Contribute to the shared mission </span> </h2> <ul> <li><strong>Model developers:</strong> <a href="https://evalcards.evalevalai.com/help/get-verified" rel="nofollow">Report verified evaluation results</a>.</li> <li><strong>Evaluation developers:</strong> Report benchmarks and run data using the <a href="https://github.com/evaleval/every_eval_ever" rel="nofollow">Every Eval Ever schema</a>.</li> <li><strong>Evaluation, governance, and policy researchers:</strong> <a href="https://evalcards.evalevalai.com/" rel="nofollow">Explore Evaluation Cards</a> by benchmark or model, or use it to examine the state of evaluation reporting as a whole.</li> </ul> <h2 class="relative group flex items-baseline"> <a id="about-the-evaleval-coalition" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/evaleval-aisi#about-the-evaleval-coalition" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> About the EvalEval Coalition </span> </h2> <p>The EvalEval Coalition is a research community developing scientifically grounded research and robust deployment infrastructure for the evaluation ecosystem. Its goal is to improve evaluation science, address the lack of consensus around documenting evaluation applicability and utility, and broaden coverage of the impacts that matter for scientific research and policy analysis.</p> <p>The coalition's flagship projects include <a href="https://evalevalai.com/projects/every-eval-ever/" rel="nofollow">Every Eval Ever</a>, a shared schema and repository for evaluation results, and <a href="https://evalevalai.com/projects/eval-cards/" rel="nofollow">Evaluation Cards</a>, which combines benchmark metadata, evaluation-run data, and model metadata into interpretable records. Together, they make it easier to understand when apparently similar scores were produced under meaningfully different conditions.</p> <h2 class="relative group flex items-baseline"> <a id="about-the-uk-ai-security-institute" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/evaleval-aisi#about-the-uk-ai-security-institute" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> About the UK AI Security Institute </span> </h2> <p>The <a href="https://www.aisi.gov.uk/" rel="nofollow">UK AI Security Institute</a> is a research organisation within the UK government's Department for Science, Innovation and Technology. Its mission is to equip governments with a scientific understanding of the risks posed by advanced AI. AISI conducts research and builds infrastructure to understand advanced AI capabilities and impacts, develop and test mitigations, and inform policy.</p> <h2 class="relative group flex items-baseline"> <a id="further-reading" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/evaleval-aisi#further-reading" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Further reading </span> </h2> <ul> <li><a href="https://arxiv.org/abs/2606.17930" rel="nofollow"><em>How Inference Compute Shapes Frontier LLM Evaluation</em></a></li> <li><a href="https://www.aisi.gov.uk/blog/hibayes-improving-llm-evaluation-with-hierarchical-bayesian-modelling" rel="nofollow">HiBayES: Improving LLM evaluation with hierarchical Bayesian modelling</a></li> <li><a href="https://arxiv.org/abs/2505.05602" rel="nofollow">HiBayES paper</a></li> <li><a href="https://arxiv.org/abs/2608.14425" rel="nofollow">OptStop paper</a></li> <li><a href="https://evalevalai.com/projects/every-eval-ever/" rel="nofollow">Every Eval Ever</a></li> <li><a href="https://evalcards.evalevalai.com/" rel="nofollow">Evaluation Cards</a></li> </ul> <!----></div>

2026/9/22
阅读更多

Transformers now runs llama.cpp quants

<div class="SVELTE_HYDRATER contents" data-target="RepoCodeCopy" data-props="{}"><div></div></div> <div> <a target="_blank" class="btn mb-5 font-sans text-sm no-underline" href="https://github.com/huggingface/blog/blob/main/transformers-llama-cpp-quants.md">Update on GitHub</a></div> <div class="relative overflow-clip [overflow-clip-margin:3rem] [&amp;>:last-child]:mb-0"> <p></p><strong>We're adding support for running GGUF models efficiently in transformers</strong>, so you can use checkpoints sized for your laptop's memory through the familiar transformers APIs. Pick a GGUF from the Hub, load it with <code>from_pretrained</code>, and start generating on your own machine.<p></p> <video controls="" width="100%"> <source src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/transformers-llama-cpp-quants/transformers-gguf.mp4" type="video/mp4"> </video> <p>Running AI models on your laptop has become much easier, and <a href="https://github.com/ggml-org/llama.cpp" rel="nofollow">llama.cpp</a> has been a big part of that. Its inference engine powers local AI tools such as Ollama, LM Studio, and Jan. Alongside projects like <a href="https://github.com/ml-explore/mlx" rel="nofollow">MLX</a>, it has helped make local inference a practical option for everyday use.</p> <p><em>A recent example of what local AI can feel like:</em></p> <blockquote class="twitter-tweet" data-conversation="none"><p lang="en" dir="ltr">This is where we are right now. And i’m not gonna lie it feels pretty magical 🧙‍♀️<br><br>Qwen3.6 27B running inside of Pi coding agent via Llama.cpp on the MacBook Pro<br><br>For non-trivial tasks on the <a href="https://x.com/huggingface?ref_src=twsrc%5Etfw" rel="nofollow">@huggingface</a> codebases, this feels very, very close to hitting the latest Opus in Claude… <a href="https://t.co/lsIxLoUneU" rel="nofollow">pic.twitter.com/lsIxLoUneU</a></p>— Julien Chaumond (@julien_c) <a href="https://x.com/julien_c/status/2047647522173104145?ref_src=twsrc%5Etfw" rel="nofollow">April 24, 2026</a></blockquote> <p><strong>GGUF</strong>, developed by the llama.cpp team, is a widely used format for local inference. The team also shares quantized checkpoints under <a href="https://huggingface.co/ggml-org">ggml-org on the Hub</a>. Publishers such as <a href="https://huggingface.co/unsloth">Unsloth</a>, <a href="https://huggingface.co/lmstudio-community">LM Studio Community</a>, and <a href="https://huggingface.co/bartowski">bartowski</a> also provide ready-to-use GGUF checkpoints in a range of quantizations, so users can pick the version that fits their machine. GGUF models have been downloaded millions of times.</p> <p>We want to make it easier to run these models locally with transformers, too. Compatibility is only useful if the model is pleasant to run. To bring performance close to llama.cpp, we're reusing its underlying ggml kernels through the <a href="https://huggingface.co/docs/kernels/index"><code>kernels</code></a> library, and reducing overhead in <code>generate</code>. Our initial focus is local inference on Apple Silicon, starting with the Qwen3.5 architecture.</p> <h2 class="relative group flex items-baseline"> <a id="what-is-the-gguf-file-format" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/transformers-llama-cpp-quants#what-is-the-gguf-file-format" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> What is the GGUF file format? </span> </h2> <p><a href="https://github.com/ggml-org/ggml/blob/master/docs/gguf.md" rel="nofollow">GGUF</a> packages model weights and metadata, including tokenizer information and an optional chat template, in one file. It supports different quantization levels, letting you trade some precision for a smaller memory footprint. Variants such as <code>Q4_K_M</code> mix tensor precisions, using mostly 4-bit weights while keeping sensitive tensors at higher precision.</p> <p>Here's how quantization changes the file size of <a href="https://huggingface.co/unsloth/Qwen3.5-4B-GGUF/tree/main">Unsloth's Qwen3.5-4B</a>:</p> <div class="max-w-full overflow-auto"> <table> <thead><tr> <th>GGUF variant</th> <th align="right">File size</th> <th>Tradeoff</th> </tr> </thead><tbody><tr> <td><code>BF16</code></td> <td align="right">8.42 GB</td> <td>Unquantized reference</td> </tr> <tr> <td><code>Q6_K</code></td> <td align="right">3.53 GB</td> <td>More precision than the smaller variants</td> </tr> <tr> <td><code>Q5_K_M</code></td> <td align="right">3.14 GB</td> <td>A middle ground between size and precision</td> </tr> <tr> <td><code>Q4_K_M</code></td> <td align="right">2.74 GB</td> <td>A practical starting point for local inference</td> </tr> </tbody> </table> </div> <p>We suggest starting with <code>Q4_K_M</code>, then trying <code>Q5_K_M</code> or <code>Q6_K</code> if you have more memory available. More aggressive quantization can help larger models fit, but the quality tradeoff depends on the model and the task. Evaluate it on the work you actually want the model to do. The <a href="https://huggingface.co/docs/hub/gguf#quantization-types">Hub's GGUF documentation</a> describes the available quantization types.</p> <h2 class="relative group flex items-baseline"> <a id="load-gguf-with-transformers" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/transformers-llama-cpp-quants#load-gguf-with-transformers" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Load GGUF with transformers </span> </h2> <p>To get started, you need:</p> <ul> <li><strong>An Apple Silicon Mac</strong>.</li> <li><strong>A PyTorch version supported by the published <a href="https://huggingface.co/kernels/ggml-org/ggml-quantization">ggml-quantization kernel builds</a></strong>, usually the two latest PyTorch releases.</li> <li><strong>The latest version of transformers (main for now, until the next release) and a compatible version of <code>kernels</code></strong>.</li> </ul> <pre><code class="language-bash">pip install -U <span class="hljs-string">"git+https://github.com/huggingface/transformers.git"</span> kernels </code></pre> <p>To load a GGUF model, pass its Hub <code>model_id</code> and filename as <code>gguf_file</code> to <code>from_pretrained</code>.</p> <p>No extra configuration is needed: when the weights stay packed on Metal, transformers automatically loads the compatible ggml/Metal layer kernels and uses <code>ggml-org/ggml-attn</code> as the attention implementation. If that kernel cannot be fetched, the model falls back to <code>"sdpa"</code> with a warning, and you can always force <code>"sdpa"</code> by passing <code>attn_implementation="sdpa"</code> explicitly. See the <a href="https://huggingface.co/docs/transformers/main/en/quantization/gguf">GGUF documentation</a> for more loading options.</p> <pre><code class="language-python"><span class="hljs-keyword">import</span> torch <span class="hljs-keyword">from</span> transformers <span class="hljs-keyword">import</span> AutoModelForCausalLM, AutoTokenizer model_id = <span class="hljs-string">"unsloth/Qwen3.5-4B-GGUF"</span> filename = <span class="hljs-string">"Qwen3.5-4B-Q4_K_M.gguf"</span> tokenizer = AutoTokenizer.from_pretrained(model_id, gguf_file=filename) model = AutoModelForCausalLM.from_pretrained( model_id, gguf_file=filename ) </code></pre> <p>That is the only GGUF-specific step. Everything after it is the standard transformers API:</p> <pre><code class="language-python">messages = [{<span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>, <span class="hljs-string">"content"</span>: <span class="hljs-string">"Explain why the sky is blue in a few sentences."</span>}] inputs = tokenizer.apply_chat_template( messages, tokenize=<span class="hljs-literal">True</span>, add_generation_prompt=<span class="hljs-literal">True</span>, return_dict=<span class="hljs-literal">True</span>, return_tensors=<span class="hljs-string">"pt"</span>, ).to(model.device) <span class="hljs-keyword">with</span> torch.inference_mode(): outputs = model.generate(**inputs, max_new_tokens=<span class="hljs-number">256</span>) <span class="hljs-built_in">print</span>(tokenizer.decode(outputs[<span class="hljs-number">0</span>], skip_special_tokens=<span class="hljs-literal">True</span>)) </code></pre> <blockquote class="warning"> <p>Without a compatible quantization kernel, the loader falls back to dequantizing the model and uses more memory.</p> </blockquote> <h2 class="relative group flex items-baseline"> <a id="serve-gguf-with-your-preferred-interface" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/transformers-llama-cpp-quants#serve-gguf-with-your-preferred-interface" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Serve GGUF with your preferred interface </span> </h2> <p>You can also use the same checkpoint with <a href="https://huggingface.co/docs/transformers/main/en/serve-cli/serving"><code>transformers serve</code></a>, which exposes an OpenAI-compatible API:</p> <pre><code class="language-bash">pip install -U <span class="hljs-string">"transformers[serving] @ git+https://github.com/huggingface/transformers.git"</span> kernels transformers serve <span class="hljs-string">"unsloth/Qwen3.5-4B-GGUF:Qwen3.5-4B-Q4_K_M.gguf"</span> </code></pre> <p>The model argument uses <code>&lt;model_id&gt;:&lt;filename&gt;.gguf</code>: before the colon is the Hub repository (<code>unsloth/Qwen3.5-4B-GGUF</code>), and after it is the file to load (<code>Qwen3.5-4B-Q4_K_M.gguf</code>). This selects a specific quantization from a repository that may contain several.</p> <p>For models whose chat template supports thinking, add <code>--reasoning off</code> to skip it or <code>--reasoning on</code> to enable it. The default, <code>--reasoning auto</code>, follows the chat template’s default. See the <a href="https://huggingface.co/docs/transformers/main/en/serve-cli/serving#enable-reasoning-on-the-server">reasoning options</a> for details.</p> <p>You can connect a client such as <a href="https://www.jan.ai/docs/desktop/remote-models/custom-endpoint" rel="nofollow">Jan</a> or <a href="https://pi.dev/" rel="nofollow">Pi</a> by adding a custom OpenAI-compatible provider with these settings:</p> <div class="max-w-full overflow-auto"> <table> <thead><tr> <th>Setting</th> <th>Value</th> </tr> </thead><tbody><tr> <td>Base URL</td> <td><code>http://localhost:8000/v1</code></td> </tr> <tr> <td>Model ID</td> <td><code>unsloth/Qwen3.5-4B-GGUF:Qwen3.5-4B-Q4_K_M.gguf</code></td> </tr> </tbody> </table> </div> <p>transformers runs the model on your Mac, while the client provides the conversation interface. The same endpoint can be used by other clients that support this API.</p> <h2 class="relative group flex items-baseline"> <a id="benchmarking-against-llamacpp" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/transformers-llama-cpp-quants#benchmarking-against-llamacpp" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Benchmarking against llama.cpp </span> </h2> <p>Our reference for local inference performance is llama.cpp. The comparison below focuses on three GGUF checkpoints: a small dense model, a larger dense model, and a mixture-of-experts model.</p> <p>The llama.cpp column comes from the <a href="https://github.com/ggml-org/llama.cpp/tree/master/tools/llama-bench" rel="nofollow"><code>llama-bench</code></a> tool (build <code>5f55650a7</code>, release b10200, Metal backend from ggml 0.18.0), run as <code>llama-bench -m &lt;file&gt; -p 0 -n 128 -r 3</code>, which reports <code>tg128</code>: the token-generation rate over 128 decoded tokens, averaged across three repetitions, with prompt processing excluded. The transformers column is <code>generate</code> producing the same 128 tokens from a 12-token prompt, best of three warmed runs, and it includes prefill.</p> <p>Measured on a MacBook Pro M2 Max, 32 GB unified memory, macOS 26.6, PyTorch 2.12.1, kernels 0.17.0, plugged in.</p> <details> <summary>The benchmark script</summary> <pre><code class="language-python"><span class="hljs-keyword">import</span> time <span class="hljs-keyword">import</span> torch <span class="hljs-keyword">from</span> transformers <span class="hljs-keyword">import</span> AutoModelForCausalLM, AutoTokenizer model_id, filename = <span class="hljs-string">"unsloth/Qwen3.5-4B-GGUF"</span>, <span class="hljs-string">"Qwen3.5-4B-Q4_K_M.gguf"</span> model = AutoModelForCausalLM.from_pretrained(model_id, gguf_file=filename) tokenizer = AutoTokenizer.from_pretrained(model_id, gguf_file=filename) inputs = tokenizer(<span class="hljs-string">"The capital of France is Paris. The capital of Germany is"</span>, return_tensors=<span class="hljs-string">"pt"</span>) inputs = inputs.to(model.device) <span class="hljs-keyword">with</span> torch.inference_mode(): model.generate(**inputs, max_new_tokens=<span class="hljs-number">8</span>, min_new_tokens=<span class="hljs-number">8</span>, do_sample=<span class="hljs-literal">False</span>) <span class="hljs-comment"># warm up</span> torch.mps.synchronize() <span class="hljs-keyword">for</span> _ <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(<span class="hljs-number">3</span>): time.sleep(<span class="hljs-number">90</span>) <span class="hljs-comment"># let the machine cool: back-to-back runs decay by 10% or more</span> start = time.perf_counter() model.generate(**inputs, max_new_tokens=<span class="hljs-number">128</span>, min_new_tokens=<span class="hljs-number">128</span>, do_sample=<span class="hljs-literal">False</span>) torch.mps.synchronize() <span class="hljs-built_in">print</span>(<span class="hljs-string">f"<span class="hljs-subst">{<span class="hljs-number">128</span> / (time.perf_counter() - start):<span class="hljs-number">.1</span>f}</span> tok/s"</span>) </code></pre> <p>For the other column:</p> <pre><code class="language-bash">llama-bench -hf unsloth/Qwen3.5-4B-GGUF:Q4_K_M -p 0 -n 128 -r 3 </code></pre> </details> <p><a href="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/transformers-llama-cpp-quants/benchmark-comparison.svg" rel="nofollow"><img src="https://imgproxy.citrusreader.com/xhNjHIkwyuPuUAUvHNujOy1bFRXT1q3-Z_jJJO7gdGg/raw:1/aHR0cHM6Ly9odWdnaW5nZmFjZS5jby9kYXRhc2V0cy9odWdnaW5nZmFjZS9kb2N1bWVudGF0aW9uLWltYWdlcy9yZXNvbHZlL21haW4vYmxvZy90cmFuc2Zvcm1lcnMtbGxhbWEtY3BwLXF1YW50cy9iZW5jaG1hcmstY29tcGFyaXNvbi5zdmc" alt="GGUF generation throughput compared with llama.cpp" referrerpolicy="no-referrer"></a></p> <p>Transformers is close to llama.cpp across all three checkpoints. The chart uses the same measurements described above; it does not imply identical benchmark conditions, since the Transformers measurement includes prefill while <code>llama-bench</code> reports decode-only throughput.</p> <h2 class="relative group flex items-baseline"> <a id="transformers-and-llamacpp" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/transformers-llama-cpp-quants#transformers-and-llamacpp" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> transformers and llama.cpp </span> </h2> <p>When <a href="https://huggingface.co/blog/ggml-joins-hf">GGML and llama.cpp joined Hugging Face</a>, we described their complementary roles: llama.cpp provides a foundation for local inference, while transformers provides a foundation for model definition. GGUF support brings those two closer together.</p> <p><strong>llama.cpp remains our recommended engine when your priority is efficient local inference.</strong> Its dedicated runtime, memory management, and broad hardware support are built around that goal. This integration gives developers a convenient way to work with the same GGUF checkpoints inside transformers:</p> <ul> <li><strong>Experiment with GGUF in Python and PyTorch.</strong> Inspect intermediate activations with hooks, modify a model's forward pass, or prototype custom layers using familiar PyTorch tools.</li> <li><strong>Evaluate GGUF models.</strong> Use your existing transformers evaluation workflows to measure the quality of quantized checkpoints.</li> <li><strong>Validate GGUF conversions.</strong> For us as developers, loading the original checkpoint and its GGUF conversion in transformers makes it easier to check that the weights were converted correctly, accounting for quantization error.</li> <li><strong>Try new decoding ideas.</strong> Use custom logits processors and stopping criteria with <code>generate</code>, or write your own generation loop in Python.</li> <li><strong>Fine-tune from a GGUF checkpoint.</strong> Dequantize the weights and continue with a standard transformers training workflow.</li> </ul> <p>For that last case, use <code>GgufConfig(dequantize=True)</code>:</p> <pre><code class="language-python"><span class="hljs-keyword">import</span> torch <span class="hljs-keyword">from</span> transformers <span class="hljs-keyword">import</span> AutoModelForCausalLM, GgufConfig model = AutoModelForCausalLM.from_pretrained( <span class="hljs-string">"unsloth/Qwen3.5-4B-GGUF"</span>, gguf_file=<span class="hljs-string">"Qwen3.5-4B-Q4_K_M.gguf"</span>, quantization_config=GgufConfig(dequantize=<span class="hljs-literal">True</span>), dtype=torch.bfloat16, ) </code></pre> <h2 class="relative group flex items-baseline"> <a id="beyond-gguf-ggml-kernels-for-more-models" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/transformers-llama-cpp-quants#beyond-gguf-ggml-kernels-for-more-models" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Beyond GGUF: ggml kernels for more models </span> </h2> <p><strong>The bigger opportunity is bringing ggml's performance to models that llama.cpp does not support.</strong></p> <p>transformers already provides the PyTorch implementations of these architectures. With ggml kernels and quantization schemes available in PyTorch, we can work toward accelerating their supported operations without first implementing the entire model in llama.cpp. This is especially useful for new architectures, research models, and custom variants that may never receive a dedicated llama.cpp implementation.</p> <p>That opportunity extends beyond the GGUF format itself. A kernel operates on tensors; it does not require the whole model to come from a GGUF file. The same building blocks can be integrated into other transformers models and loading workflows. This also opens a path to other modalities: computer vision models, audio models, and multimodal models could reuse compatible attention, normalization, and matrix multiplication kernels without first having a full implementation in llama.cpp. Each architecture still needs integration and validation; the initial GGUF examples here cover text generation.</p> <h2 class="relative group flex items-baseline"> <a id="fast-local-inference-with-python-and-pytorch" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/transformers-llama-cpp-quants#fast-local-inference-with-python-and-pytorch" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Fast local inference with Python and PyTorch </span> </h2> <p>We also wanted to show how far we can get while keeping the model and generation loop in Python. <strong>With the right kernels and an efficient generation loop, Python and PyTorch can deliver strong local inference performance.</strong> The kernels handle the heavy computation, while the generation loop keeps the GPU busy by avoiding unnecessary synchronization.</p> <p>Our focus was to make eager execution fast without requiring <code>torch.compile</code>. For interactive use, we wanted a quick start and a steady stream of tokens, without compilation pauses or recompilation when input shapes change. The two main pieces of that work are the kernels and <code>generate</code> itself.</p> <h3 class="relative group flex items-baseline"> <a id="reusing-ggmls-metal-kernels" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/transformers-llama-cpp-quants#reusing-ggmls-metal-kernels" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Reusing ggml's Metal kernels </span> </h3> <p>A kernel is a small program that performs an operation on the GPU. PyTorch supplies general-purpose implementations; a specialized kernel can do less work, combine several operations, or read quantized weights directly in their stored format.</p> <p>The <code>kernels</code> library lets us distribute compatible builds of ggml's Metal kernels on the Hub and call them from transformers. That brings ggml's work into the PyTorch model without replacing the model with a separate inference runtime.</p> <div class="max-w-full overflow-auto"> <table> <thead><tr> <th>Kernel</th> <th>What it does</th> </tr> </thead><tbody><tr> <td><a href="https://huggingface.co/kernels/ggml-org/ggml-quantization"><code>ggml-quantization</code></a></td> <td>Reads packed quantized weights for matrix operations, including the selected experts in an MoE model. It avoids expanding the whole weight matrix before each decode operation.</td> </tr> <tr> <td><a href="https://huggingface.co/kernels/ggml-org/ggml-norm"><code>ggml-norm</code></a></td> <td>Fuses normalization operations, including the zero-centered RMSNorm used by Qwen3.5 and Qwen3.8.</td> </tr> <tr> <td><a href="https://huggingface.co/kernels/ggml-org/ggml-attn"><code>ggml-attn</code></a></td> <td>Provides ggml's Metal flash attention for prompt processing and token decoding.</td> </tr> <tr> <td><a href="https://huggingface.co/kernels/ggml-org/ggml-gated-delta-net"><code>ggml-gated-delta-net</code></a></td> <td>Accelerates the gated delta network used in the linear-attention layers of the Qwen3.5 and Qwen3.8 hybrid architectures.</td> </tr> <tr> <td><a href="https://huggingface.co/kernels/transformers-community/topk"><code>topk</code></a></td> <td>Selects the experts for each token in an MoE model, combining softmax and top-k routing. This is our own Metal implementation.</td> </tr> </tbody> </table> </div> <p>The first four packages build on ggml's kernels; the top-k kernel addresses a separate bottleneck in MoE routing. Together they reduce the GPU work needed for each generated token.</p> <p>To show the contribution of the layer kernels, we compare the same packed GGUF checkpoints with and without them. The quantization kernel stays enabled in both configurations: disabling it would also change how weights are represented and would measure a different tradeoff.</p> <p><a href="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/transformers-llama-cpp-quants/layer-kernel-benchmark.svg" rel="nofollow"><img src="https://imgproxy.citrusreader.com/ffZmBjXvJQkutpRPGnGmvTInJPdyVkvj--4aKthIk7I/raw:1/aHR0cHM6Ly9odWdnaW5nZmFjZS5jby9kYXRhc2V0cy9odWdnaW5nZmFjZS9kb2N1bWVudGF0aW9uLWltYWdlcy9yZXNvbHZlL21haW4vYmxvZy90cmFuc2Zvcm1lcnMtbGxhbWEtY3BwLXF1YW50cy9sYXllci1rZXJuZWwtYmVuY2htYXJrLnN2Zw" alt="Throughput improvement from the layer kernels" referrerpolicy="no-referrer"></a></p> <h3 class="relative group flex items-baseline"> <a id="keeping-the-cpu-and-gpu-working-together" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/transformers-llama-cpp-quants#keeping-the-cpu-and-gpu-working-together" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Keeping the CPU and GPU working together </span> </h3> <p>Faster kernels only help if the GPU has work to do. During generation, the CPU schedules GPU operations and controls the loop that produces the next token. Reading a result back from the GPU can force the CPU to wait until queued operations finish. Repeating even a small wait for every token can noticeably reduce throughput.</p> <p>Two changes address this in <code>generate</code>, which results in improvements for all transformers models (not just when running GGUF files):</p> <ul> <li><strong><a href="https://github.com/huggingface/transformers/pull/48814" rel="nofollow">Drop an unnecessary attention mask early (#48814)</a>.</strong> When a supported decoder-only input has no padding, its all-ones padding mask can be removed at the start of generation. Downstream attention code no longer needs to inspect that mask repeatedly to determine whether it can be skipped. Causal attention is still preserved.</li> <li><strong><a href="https://github.com/huggingface/transformers/pull/47975" rel="nofollow">Defer the stopping check (#47975)</a>.</strong> On supported paths, <code>generate</code> copies the stopping decision asynchronously and consumes it on the following step. The CPU can keep scheduling work while the GPU runs. Streaming tokens use the same approach, and any extra step past the stopping condition is removed from the result.</li> </ul> <p>These changes improve the generation loop around the model, so their usefulness extends beyond GGUF. They complement the kernel work: kernels reduce the cost of an operation, while fewer synchronization points let CPU scheduling and GPU execution overlap.</p> <p><a href="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/transformers-llama-cpp-quants/generation-loop-benchmark.svg" rel="nofollow"><img src="https://imgproxy.citrusreader.com/hTOsFPFCOeZTZv5IcQfKHrplvnhPUohoZ5wOd_Y3_tw/raw:1/aHR0cHM6Ly9odWdnaW5nZmFjZS5jby9kYXRhc2V0cy9odWdnaW5nZmFjZS9kb2N1bWVudGF0aW9uLWltYWdlcy9yZXNvbHZlL21haW4vYmxvZy90cmFuc2Zvcm1lcnMtbGxhbWEtY3BwLXF1YW50cy9nZW5lcmF0aW9uLWxvb3AtYmVuY2htYXJrLnN2Zw" alt="Throughput improvement from the generation loop changes" referrerpolicy="no-referrer"></a></p> <p>These measurements keep all layer kernels enabled; the bars isolate the changes to the generation loop.</p> <h2 class="relative group flex items-baseline"> <a id="current-limitations-and-next-steps" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/transformers-llama-cpp-quants#current-limitations-and-next-steps" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Current limitations and next steps </span> </h2> <p>The initial target is a single interactive conversation on Apple Silicon. There are a few boundaries to keep in mind:</p> <ul> <li><strong>The packed inference path is MPS-only for now.</strong> GGUF import through dequantization remains a separate option; support for the file format does not imply that packed kernels are available on every device.</li> <li><strong>Padding and batching still need work.</strong> Unpadded inputs benefit from the mask optimization described above. Padded batches cannot take the same shortcut and can have lower performance. We want to extend the work to <code>generate_batch</code> on MPS.</li> <li><strong>Architecture coverage is limited.</strong> The packed loader currently covers the Qwen3.5 dense and MoE architectures, including compatible Qwen3.8 checkpoints. Adding support for other architectures is relatively straightforward, and we’ll expand coverage gradually.</li> </ul> <p>If you have a GGUF model you would like to use in transformers, <a href="https://github.com/huggingface/transformers/issues" rel="nofollow">open an issue</a> with the checkpoint and your use case. That will help us prioritize support for the models people are running locally.</p> <h2 class="relative group flex items-baseline"> <a id="acknowledgments" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/transformers-llama-cpp-quants#acknowledgments" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Acknowledgments </span> </h2> <p>We would like to thank <a href="https://huggingface.co/ArthurZ">Arthur Zucker</a> for initiating this work and reviewing all of my PRs, and <a href="https://huggingface.co/cyrilvallez">Cyril Vallez</a> for the <code>generate</code> PRs. We are grateful to <a href="https://huggingface.co/sayakpaul">Sayak Paul</a>, the <a href="https://github.com/ggml-org/llama.cpp" rel="nofollow">llama.cpp team</a>, and Bertrand Chevalier for their help integrating the kernels. We also thank <a href="https://huggingface.co/ariG23498">Aritra Roy Gosthipaty</a> and <a href="https://huggingface.co/pcuenq">Pedro Cuenca</a> for reviewing this blog post, and <a href="https://huggingface.co/lysandre">Lysandre Debut</a> for overseeing the project.</p> </div>

2026/9/22
阅读更多

Jun Kim, oMLX creator and maintainer, joins Hugging Face to support the MLX community

<div class="SVELTE_HYDRATER contents" data-target="RepoCodeCopy" data-props="{}"><div></div><!----></div> <div> <!--[0--><a target="_blank" class="btn mb-5 font-sans text-sm no-underline" href="https://github.com/huggingface/blog/blob/main/omlx.md">Update on GitHub</a><!--]--></div><!----> <!----> <!--[-1--><!--]--> <div class="relative overflow-clip [overflow-clip-margin:3rem] [&amp;>:last-child]:mb-0"><!----> <p><!--[--><!--[0--></p><!--]--><!--]-->We are super excited to welcome Jun as our newest team member 🔥. We are completely invested in local AI, and MLX is a central piece of the ecosystem. We are delighted that Jun chose us to set up home and continue contributing to MLX.<p></p> <p>MLX is Apple's framework for local AI, especially optimized for Apple Silicon. We are big MLX supporters since it was the Christmas present from <a href="https://x.com/awnihannun/status/1732184443451019431?s=20" rel="nofollow">Awni</a> and <a href="https://x.com/angeloskath/status/1732304844403429477?s=20" rel="nofollow">Angelos</a> in 2023, and proud that Hugging Face is the Hub where people find MLX models and contribute their own. Usage of open, local AI is accelerating, and we believe in a healthy ecosystem where people can find the tools that work for them.</p> <h2 class="relative group flex items-baseline"> <a id="what-is-the-impact-for-omlx" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/omlx#what-is-the-impact-for-omlx" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> What is the impact for oMLX? </span> </h2> <p>Stability, and hopefully faster development! Graduating from a side job to a fully maintained and funded project will allow Jun to better guide the contributors and build for the long-term. <a href="https://github.com/jundot/omlx" rel="nofollow">oMLX</a> stays Apache 2.0, and Jun keeps leading it as before.</p> <h2 class="relative group flex items-baseline"> <a id="what-is-the-impact-for-mlx-at-large" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/omlx#what-is-the-impact-for-mlx-at-large" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> What is the impact for MLX at large? </span> </h2> <p>Our end goal is to unblock the community to run local AI in any shape or form, and provide the tools and building blocks to make that happen. We expect oMLX to serve as a testbed for new ideas, while leveraging the foundational work of the dependencies it already relies upon, such as <a href="https://github.com/ml-explore/mlx-lm" rel="nofollow">mlx-lm</a> or <a href="https://github.com/Blaizzy/mlx-vlm" rel="nofollow">mlx-vlm</a>. We believe that strong modeling and inference libraries help the community, so we'd love to upstream work to wherever it makes sense. We have been collaborating with many projects mlx-lm, mlx-vlm, LMStudio, and we hope we can strengthen the relationship with <a href="https://x.com/zcbenz" rel="nofollow">Cheng</a>, <a href="https://x.com/Prince_Canuma" rel="nofollow">Prince</a>, <a href="https://x.com/yagilb" rel="nofollow">Yagil</a>, and their teams to better serve the community together.</p> <p>Concretely, one focus area is the quick transition from a transformers model definition to a reference MLX implementation that can be consumed by different engines, so each one can focus on the unique features they provide. The transformers library has become the reference for ML model definitions, we want to streamline the process to make new transformers models run on MLX.</p> <p>We are incredibly excited about the future.</p> <p>Welcome, Jun! 🙌</p> <!----></div>

2026/9/22
阅读更多

Pruning LLMs Like a Physicist: Block Removal as an Ising Optimization Problem

<div class="SVELTE_HYDRATER contents" data-target="RepoCodeCopy" data-props="{}"><div></div></div> <div> </div> <div class="relative overflow-clip [overflow-clip-margin:3rem] [&amp;>:last-child]:mb-0"> <p></p>One of the cheapest ways to make a large language model faster is also one of the bluntest: delete whole transformer blocks. Because the model literally gets shorter, <a href="https://huggingface.co/papers/2602.00161">block removal</a> (also called depth pruning) buys predictable inference speedups on top of the memory savings, and it stacks cleanly with quantization, low-rank compression, and other techniques. The hard part is deciding <em>which</em> blocks to cut. Remove the wrong ones and the model collapses; and the effect of removing any one block depends on which others you remove alongside it, so the choices interact. That makes it a combinatorial problem, not a ranking problem, and combinatorial problems with interacting binary variables are exactly what the physics of spin systems was built to describe.<p></p> <p>Our latest paper, <em><a href="https://huggingface.co/papers/2602.00161">LLM Compression by Block Removal with Constrained Binary Optimization</a></em>, takes that correspondence literally. We reformulate block selection as a constrained binary optimization (CBO) problem that maps directly onto an <strong>Ising glass</strong>, a disordered spin system with all-to-all interactions and a fixed number of "up" spins. The energy of that spin system turns out to be a strong, cheap proxy for how well the pruned model will actually score on benchmarks, which means we can rank a huge number of candidate configurations without benchmarking any of them, and hand the hard instances to the same classical and quantum-inspired solvers we use elsewhere at Multiverse. The payoff in the deep-compression regime is large: at 50% compression of Llama-3.3-70B-Instruct, we gain almost <strong>23 percentage points on MMLU</strong> over the best competing block-removal method.</p> <h2 class="relative group flex items-baseline"> <a id="why-picking-blocks-is-a-many-body-problem" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/MultiverseComputingCAI/pruning-llms-like-a-physicist-block-removal-as-an#why-picking-blocks-is-a-many-body-problem" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Why picking blocks is a many-body problem </span> </h2> <p>Most existing block-removal methods score each block on its own, then remove the ones that look least important, using magnitude, sensitivity, or "block influence" heuristics. In physics terms these are <em>mean-field</em> methods: they treat each block as if its contribution were independent of the others, the way mean-field theory replaces a spin's neighbors with a single averaged field. A related shortcut is to only ever remove a single consecutive run of blocks, which keeps the problem small but throws away most of the search space.</p> <p>The trouble is that blocks are not independent, any more than spins in a real magnet are. Whether removing block 20 hurts the model depends on whether you also removed block 19 or block 24, an interaction, or <em>coupling</em>, between the two decisions. As models get deeper and more heterogeneous, ignoring those couplings leaves quality on the table, especially when you want to remove a lot of blocks at once. What you really want is to search over <em>combinations</em> of blocks while accounting for how they interact, but the number of combinations grows exponentially, so brute force looks hopeless. This is precisely the regime, exponentially large configuration spaces with pairwise couplings, where the tools of statistical physics earn their keep.</p> <h2 class="relative group flex items-baseline"> <a id="the-idea-turn-block-selection-into-an-energy-minimization-problem" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/MultiverseComputingCAI/pruning-llms-like-a-physicist-block-removal-as-an#the-idea-turn-block-selection-into-an-energy-minimization-problem" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> The idea: turn block selection into an energy-minimization problem </span> </h2> <p>We attach a binary variable to each transformer block: 0 means keep it, 1 means remove it, just like a spin that can point down or up. Then we do a second-order Taylor expansion of the model's loss with respect to those variables, which produces an (approximate) Hessian matrix. The diagonal of that Hessian is how much each block matters on its own; the off-diagonal entries are exactly the pairwise <em>couplings</em> between blocks, the many-body physics that mean-field methods throw away.</p> <p>That reformulation turns "which blocks should I remove?" into a clean optimization: find the set of <em>M</em> blocks whose removal minimizes the energy <code>xᵀH⁰x</code>, subject to removing exactly <em>M</em> of the <em>N</em> blocks. Mathematically this is a constrained binary optimization problem; physically it is an Ising glass, an all-to-all coupled spin system with conserved magnetization (the fixed number of removed blocks plays the role of a fixed total spin). The key property we establish is that this energy is a strong proxy for downstream quality: <strong>low-energy states of the spin system correspond to high-performing pruned models.</strong> Minimizing energy and maximizing benchmark score become the same search.</p> <p><a href="https://cdn-uploads.huggingface.co/production/uploads/668e37fd9c9aa124a3c867e8/zD5BW0R8lXV9fmVyap9Jb.png" rel="nofollow"><img src="https://imgproxy.citrusreader.com/koV3ivcmSUqJHn5_O_MYC3YDLrXlf5KoMxoJTWcx3TM/raw:1/aHR0cHM6Ly9jZG4tdXBsb2Fkcy5odWdnaW5nZmFjZS5jby9wcm9kdWN0aW9uL3VwbG9hZHMvNjY4ZTM3ZmQ5YzlhYTEyNGEzYzg2N2U4L3pENUJXMFI4bFhWOWZtVnlhcDlKYi5wbmc" alt="Sketch of the method: block removal is cast as a constrained binary optimization / Ising problem whose low-energy states correspond to high-performing pruned models." referrerpolicy="no-referrer"></a></p> <p><em>Block selection becomes a constrained binary optimization problem, equivalent to finding low-energy states of an Ising glass; each solution says which M of N blocks to delete. Right: the coupling variable α we insert into each block's residual path to build the Hessian. Source: paper Figure 1.</em></p> <p>The reason this is practical is cost. The Hessian, i.e. the full set of couplings, is computed just once, from forward and backward passes on a small calibration dataset. After that, evaluating any candidate configuration is a single cheap energy calculation, no need to run the actual model, let alone benchmark it. And because the couplings don't depend on the compression target, the same Hessian can be reused to solve for many different values of <em>M</em>.</p> <h2 class="relative group flex items-baseline"> <a id="solving-it-exact-when-you-can-quantum-or-quantum-inspired-when-you-cant" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/MultiverseComputingCAI/pruning-llms-like-a-physicist-block-removal-as-an#solving-it-exact-when-you-can-quantum-or-quantum-inspired-when-you-cant" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Solving it: exact when you can, quantum or quantum-inspired when you can't </span> </h2> <p>For most models the configuration space is large but still checkable. Because computing one energy is so cheap, we brute-force it on a single GPU, checking up to tens of billions of spin configurations. A few million take seconds; the hardest tractable case here, removing 8 of Llama-3.3-70B's 80 blocks (about 29 billion configurations), took roughly two days.</p> <p>Beyond that the exact approach breaks down, and this is where casting the problem as an Ising glass pays off a second time. In its equivalent QUBO form (the constraint absorbed into a penalty term), the exact same task can be handed to the highly optimized classical, quantum, and quantum-inspired solvers built for this class of Hamiltonian, the machinery of quantum annealing, QAOA, tabu search, and specialized branch-and-bound. We find that an open-source tabu solver reliably reaches the lowest-energy states in <em>seconds</em>, even on the hardest cases we can verify against brute force. So the method scales to models where enumerating configurations is out of the question, using solvers that are squarely in Multiverse's domain.</p> <p>There's a subtle but important point here, and it runs against the usual grain of optimization. Normally a CBO or annealing solver is judged by whether it finds the true ground state. We don't actually need the ground state. What we need is a fast way to generate a handful of good low-energy states, and that is a far easier bar, which is why lightweight solvers work so well for us and why we can afford to run several of them.</p> <h2 class="relative group flex items-baseline"> <a id="why-the-whole-low-energy-spectrum-matters" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/MultiverseComputingCAI/pruning-llms-like-a-physicist-block-removal-as-an#why-the-whole-low-energy-spectrum-matters" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Why the whole low-energy spectrum matters </span> </h2> <p>The energy is a strong proxy for quality, but not a perfect one, so the single lowest-energy state isn't always the best model. This turns out to be a feature, not a bug: once the Hamiltonian is set up, reading off the ground state <em>and</em> the low-lying excited states is essentially free, giving a spectrum of high-quality candidate prunings to try rather than one fragile answer. Exploring excited states, not just the ground state, is itself an area of active physics research, and it maps neatly onto what practitioners actually need here.</p> <p>A concrete example: for Llama-3.1-8B-Instruct at 16/32 blocks removed, most of the top states cut blocks toward the end of the model, as prior work would expect. But the 17th excited state is the first to propose removing a block near the <em>beginning</em> of the model, and after light retraining that configuration outperforms the ground state across several benchmarks. That directly disproves the common assumption that the best pruning is one consecutive chunk of middle-or-late blocks, and it shows why respecting the full many-body structure of the problem pays off.</p> <p><a href="https://cdn-uploads.huggingface.co/production/uploads/668e37fd9c9aa124a3c867e8/n1GCTwe9Ulg53TAdgqjAf.png" rel="nofollow"><img src="https://imgproxy.citrusreader.com/eoSlcbZaBG3a3V3imA0Xau1m5ERbDV_vgE_xoOHKLVs/raw:1/aHR0cHM6Ly9jZG4tdXBsb2Fkcy5odWdnaW5nZmFjZS5jby9wcm9kdWN0aW9uL3VwbG9hZHMvNjY4ZTM3ZmQ5YzlhYTEyNGEzYzg2N2U4L24xR0NUd2U5VWxnNTNUQWRncWpBZi5wbmc" alt="Block-removal map and benchmark scores for the ground state versus the 17th excited state of Llama-3.1-8B-Instruct at 16/32 blocks removed." referrerpolicy="no-referrer"></a></p> <p><em>Left: which blocks each of the 20 lowest-energy states removes (red = removed). Right: the 17th excited state, which removes an early block, beats the ground state on several benchmarks after retraining. The best model is an excited state, not the ground state. Source: paper Figure 2.</em></p> <h2 class="relative group flex items-baseline"> <a id="results" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/MultiverseComputingCAI/pruning-llms-like-a-physicist-block-removal-as-an#results" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Results </span> </h2> <p>Across Llama-3.1-8B-Instruct, Qwen3-14B, and Llama-3.3-70B-Instruct, our method (CBO) is on par with or better than state-of-the-art block-removal baselines, and the gap widens as compression gets more aggressive.</p> <p>The clearest win is deep compression of Llama-3.3-70B-Instruct, evaluated without retraining. Up to 24 of 80 blocks removed, CBO is roughly on par with block influence. But at 32/80 and 40/80, it pulls decisively ahead, with an almost 23-point MMLU advantage at the deepest setting, where it beats the baseline on every benchmark we tested. For Qwen3-14B at 12/40 removed, CBO leads MMLU by about 10 points. At lighter compression the methods are comparable, which is expected: the couplings matter most when you're cutting deep.</p> <div class="max-w-full overflow-auto"> <table> <thead><tr> <th>Llama-3.3-70B-Instruct, no retraining</th> <th>Blocks removed</th> <th>MMLU</th> </tr> </thead><tbody><tr> <td>Original</td> <td>0</td> <td>82.2</td> </tr> <tr> <td>CBO (ours)</td> <td>32 / 80</td> <td><strong>76.6</strong></td> </tr> <tr> <td>Block influence</td> <td>32 / 80</td> <td>59.3</td> </tr> <tr> <td>CBO (ours)</td> <td>40 / 80</td> <td><strong>76.9</strong></td> </tr> <tr> <td>Block influence</td> <td>40 / 80</td> <td>54.0</td> </tr> </tbody> </table> </div> <p><em>At 40/80 (50% depth), CBO holds MMLU near 77 while the strongest baseline falls to the mid-50s. Source: paper Table 2.</em></p> <h2 class="relative group flex items-baseline"> <a id="it-generalizes-beyond-dense-transformers" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/MultiverseComputingCAI/pruning-llms-like-a-physicist-block-removal-as-an#it-generalizes-beyond-dense-transformers" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> It generalizes beyond dense transformers </span> </h2> <p>Block removal gets much harder on modern heterogeneous architectures, where different block types are interleaved, and the Ising formulation doesn't care: a coupling is a coupling regardless of what kind of block sits at each site. To stress-test that, we applied the method to NVIDIA-Nemotron-3-Nano-30B-A3B-FP8, a hybrid model that interleaves Mamba2, attention, and mixture-of-experts (MoE) layers in a non-uniform pattern, without any retraining.</p> <p>Nothing about our formulation assumes a homogeneous stack, so it transfers directly. Removing 2–3 MoE layers or 2 attention layers, CBO finds configurations that beat block influence on AIME25 and GPQA. The results also confirm that redundancy in these hybrid models is real but unevenly distributed: some expert layers are far more disposable than others, and the method's ability to search the coupled configuration space is what locates the good cuts. Even here, the pattern from the dense models holds, the best configuration is often an excited state rather than the ground state.</p> <h2 class="relative group flex items-baseline"> <a id="why-this-fits-multiverse-computing" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/MultiverseComputingCAI/pruning-llms-like-a-physicist-block-removal-as-an#why-this-fits-multiverse-computing" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Why this fits Multiverse Computing </span> </h2> <p>Reframing a messy machine-learning problem as an Ising Hamiltonian, then solving it with the classical and quantum-inspired optimization machinery built for physics, is squarely in Multiverse's wheelhouse, it's the same instinct that runs through our compression stack. And block removal composes with the rest of that stack, quantization, low-rank/SVD compression, width pruning, and knowledge-distillation-based healing, so it slots into a larger pipeline rather than competing with it.</p> <p>Want the full technical details, including the Taylor-expansion derivation, the QUBO mapping, the solver benchmarks, the calibration-dataset ablations, and the complete results tables? Read the full paper on <a href="https://huggingface.co/papers/2602.00161">Hugging Face</a>, or get in touch with our team to talk about applying this to your own models. The code is open-sourced at <a href="https://github.com/CompactifAI/Block_removal_through_constrained_binary_optimization" rel="nofollow">github.com/CompactifAI/Block_removal_through_constrained_binary_optimization</a>.</p> </div>

2026/9/21
阅读更多

tokenizers v1: encode, decode and scaling, measured

<div class="SVELTE_HYDRATER contents" data-target="RepoCodeCopy" data-props="{}"><div></div><!----></div> <div> <!--[0--><a target="_blank" class="btn mb-5 font-sans text-sm no-underline" href="https://github.com/huggingface/blog/blob/main/tokenizers-v1.md">Update on GitHub</a><!--]--></div><!----> <!----> <!--[-1--><!--]--> <div class="relative overflow-clip [overflow-clip-margin:3rem] [&amp;>:last-child]:mb-0"><!----> <figure class="image text-center"> <iframe src="https://lysandre-tokenizers-v1-header.static.hf.space/" width="100%" height="352" frameborder="0" sandbox="allow-scripts" referrerpolicy="same-origin"></iframe> </figure> <p><!--[--><!--[0--></p><!--]--><!--]-->The tokenizer has not historically been the bottleneck within ML workflows. Compute-wise, tokenization is light compared to the heavy modeling happening in the rest of the pipeline. Yet, in some cases, it has rapidly become key to accelerating (or slowing down) your machine learning work.<p></p> <p>As models become faster and workloads scale, that balance begins to shift. Training on massive datasets, serving many concurrent requests, or repeatedly processing long inputs can put enough pressure on the tokenizer that it starves the model of data.</p> <p>This is why we have chosen to heavily focus on performance for the upcoming version 1 of tokenizers. Tokenization should be light and should scale with your workflow. Your GPUs should never sit idle waiting for the CPU to complete its tokenization.</p> <hr> <p>In this article, we look at what makes v1 faster than v0.23, often by tens of times.</p> <p>This work was entirely possible thanks to the rest of the ecosystem. Tokenization is a very active area of open source work, and libraries such as <a href="https://github.com/marcelroed/gigatoken" rel="nofollow">gigatoken</a>, <a href="https://crates.io/crates/tiktoken-rs" rel="nofollow">tiktoken</a>, <a href="https://crates.io/crates/kitoken" rel="nofollow">kitoken</a>, <a href="https://crates.io/crates/tokie" rel="nofollow">tokie</a>, <a href="https://crates.io/crates/fastokens" rel="nofollow">fastokens</a>, <a href="https://crates.io/crates/wordchipper" rel="nofollow">wordchipper</a> and <a href="https://www.npmjs.com/package/ai-tokenizer" rel="nofollow">ai-tokenizer</a>, as well as many others, have each pushed on what a fast tokenizer can be. We read that work, and several of the ideas below reached us because another project showed they were worth trying. </p> <p>Before this refactor, tokenizers was nowhere near the performance it could have had, so contributing to it may not have seemed worth it. With this refactor, we hope to make clear that we intend tokenizers to be a library worth contributing to.</p> <p>We also thank IBM, NVIDIA, and the ExecuTorch team for contributing patches and helping us test across a wide range of hardware to broaden platform support.</p> <h2 class="relative group flex items-baseline"> <a id="results" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/tokenizers-v1#results" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Results </span> </h2> <p>We showcase results for the release candidate of tokenizers v1 against other widely used alternatives. We go over single-threaded, multi-threaded, scaling across threads, per-model comparison, per-language comparison, latency, decoding throughput, memory heap, as well as crate size.</p> <p>We run this from the <a href="https://github.com/huggingface/tokbench" rel="nofollow">tokbench</a> repository, and add a command to rerun the benchmarks on your hardware if you would like to do so.</p> <figure class="image text-center"> <iframe src="https://lysandre-tokenizers-v1-results.static.hf.space/" width="100%" height="2615" frameborder="0" sandbox="allow-scripts" referrerpolicy="same-origin"></iframe> </figure> <h2 class="relative group flex items-baseline"> <a id="what-v1-is" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/tokenizers-v1#what-v1-is" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> What V1 Is </span> </h2> <p>v1 will produce the same token IDs as v0.23. The goal was to preserve the output, the API, the vocabulary and the merge ranks, and improve everything that <strong>can</strong> be improved. That includes breadth. The library stays general across tokenizer families rather than specialising on BPE, so v1 loads everything v0.23 loaded.</p> <p>A tokenizer converts text into the list of integers a model reads. tokenizers runs that conversion in four stages. Normalization applies operations such as lowercasing or Unicode normalization to the raw text. Pre-tokenization splits the text into smaller pieces called pre-tokens. The model turns each pre-token into tokens and maps them to IDs in its vocabulary. Post-processing adds any special tokens the model expects.</p> <p>The model stage is where most of the work described here happens. Eight of the ten model families measured in this article use byte pair encoding, or BPE. BPE starts from the bytes of a pre-token and repeatedly joins the highest ranked adjacent pair until no ranked pair remains. The ranking is learned when the tokenizer is trained and ships with it, so the same text always produces the same IDs. A merge never crosses a pre-token boundary. The other two families use WordPiece and Unigram, the two other model types the library supports.</p> <p>The <a href="https://huggingface.co/docs/tokenizers/pipeline">tokenization pipeline</a> page documents the four stages. <a href="https://huggingface.co/docs/transformers/tokenizer_summary">Tokenization algorithms</a> documents BPE, WordPiece and Unigram.</p> <figure class="image text-center"> <iframe src="https://lysandre-tokenizers-v1-pipeline.static.hf.space/" width="100%" height="341" frameborder="0" sandbox="allow-scripts" referrerpolicy="same-origin"></iframe> </figure> <p>Each stage was worked on. These are the changes that mattered:</p> <div class="max-w-full overflow-auto"> <table> <thead><tr> <th>change</th> <th>what it does</th> </tr> </thead><tbody><tr> <td>workspace split</td> <td>one crate became a workspace: <code>tk-encode</code> is the required runtime, and <code>tk-serialize</code>, <code>tk-convert</code> and <code>tk-train</code> are linked only when an application needs them</td> </tr> <tr> <td>no-alloc model</td> <td>the merge working set lives in a caller-owned scratch buffer; the loop never touches the allocator</td> </tr> <tr> <td>bitcannon</td> <td>the split pattern becomes Boolean operations over bitstreams, using SIMD instructions to find splits instead of a regex engine</td> </tr> <tr> <td>merge-loop rewrite</td> <td>the pieces being merged form an intrusive doubly-linked list inside one preallocated buffer, so a merge updates two indices instead of moving data</td> </tr> <tr> <td>word cache</td> <td>a thread-local memo from pre-token bytes to finished ids, so a repeated word is merged once</td> </tr> <tr> <td>native parallelism</td> <td>one shared tokenizer encodes from many threads at once; each thread draws its scratch buffer and word cache from its own sub-pool, so threads no longer queue on a single lock (<a href="https://github.com/huggingface/tokenizers/pull/2365" rel="nofollow">#2365</a>)</td> </tr> </tbody> </table> </div> <h3 class="relative group flex items-baseline"> <a id="the-split-bitstreams-instead-of-a-regex" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/tokenizers-v1#the-split-bitstreams-instead-of-a-regex" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> The Split: Bitstreams Instead Of A Regex </span> </h3> <p>BPE models use a regular expression to split the input text into smaller, easier to process chunks called pre-tokens. Merges happen inside a pre-token and never across the boundary between two of them, so this split decides what the rest of the pipeline sees.</p> <p>That regular expression is a fixed parameter of the model. It ships with the tokenizer and never changes at runtime, so there is no need for a general-purpose regex engine to interpret it on every encode. An equivalent splitting function can be written by hand, once, for the pattern a given model actually uses.</p> <p>A hand-written function can then use the SIMD instructions (single instruction, multiple data) of a modern CPU, which apply one operation to many bytes at once and suit UTF-8 text well. bitcannon views the input's bytes as parallel streams of bits, so boundaries fall out of boolean operations across whole registers instead of a scan that advances one character at a time. It decides 64 bytes per register operation. The same idea drives <a href="https://www.cs.sfu.ca/~ashriram/papers/2012_HPCA_Parabix.pdf" rel="nofollow">Parabix</a> for text processing and <a href="https://arxiv.org/abs/1902.08318" rel="nofollow">simdjson</a> for JSON.</p> <p>This depends on recognising the pattern. A handful of grammars cover most byte-level BPE models, and a tokenizer whose pattern is not among them keeps the regex path and none of this speed-up. That is why the gains above vary as much as they do.</p> <figure class="image text-center"> <iframe src="https://lysandre-tokenizers-v1-split.static.hf.space/" width="100%" height="304" frameborder="0" sandbox="allow-scripts" referrerpolicy="same-origin"></iframe> </figure> <h3 class="relative group flex items-baseline"> <a id="the-word-cache" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/tokenizers-v1#the-word-cache" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> The Word Cache </span> </h3> <p>Real text contains many repeated words. Because BPE always produces the same token IDs for a given pre-token, v1 can save the result after processing it once. A thread-local cache maps each pre-token's bytes to its token IDs, allowing later occurrences to skip the merge process.</p> <p>Naturally, as the input grows, the number of unique words can grow more slowly than the total number of words. Repeated words then account for an increasing share of the input. New words still appear, which accounts for the occasional misses in the animation below.</p> <figure class="image text-center"> <iframe src="https://lysandre-tokenizers-v1-cache.static.hf.space/" width="100%" height="976" frameborder="0" sandbox="allow-scripts" referrerpolicy="same-origin"></iframe> </figure> <p>Reproduce the shared-prefix result with:</p> <pre><code class="language-bash">tokbench measure prefix-sharing \ --engine pipeline \ --engine hf-tokenizers \ --compare-to pipeline-no-cache \ --corpus agentic_swe </code></pre> <p>Caching works best when the input contains repeated pre-tokens. Input with few repeated pre-tokens can pay for lookups without receiving many hits.</p> <h3 class="relative group flex items-baseline"> <a id="the-merge-loop" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/tokenizers-v1#the-merge-loop" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> The Merge Loop </span> </h3> <p>The next major cost comes from the BPE merge loop. For each pre-token, the loop repeatedly finds the highest-priority adjacent pair and merges it. The previous implementation allocated new memory for every call and built a new priority queue for every pre-token.</p> <p>v1 reuses a scratch buffer owned by the caller, removing those repeated allocations. It stores symbols in a flat array and links adjacent symbols by their positions in that array, which makes updates during merging cheaper. It also processes a batch of pre-tokens in a single model call.</p> <p>Each candidate pair is also packed into a single 64-bit value, with the merge rank in the high bits. Comparing two candidates is then just comparing two integers, and "no merge here" is the largest possible value, so the loop finds its next merge without a branch.</p> <h2 class="relative group flex items-baseline"> <a class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/tokenizers-v1#method" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Method </span> </h2> <p>Small differences in benchmark design can produce large differences in tokenizer performance. We used the following rules to keep the comparison consistent across engines.</p> <div class="max-w-full overflow-auto"> <table> <thead><tr> <th>rule</th> <th>why</th> </tr> </thead><tbody><tr> <td>one timing loop</td> <td>every engine runs the identical loop; no per-engine fast path</td> </tr> <tr> <td>load excluded</td> <td>vocabulary load is timed separately, never inside encode</td> </tr> <tr> <td>id-hash verified</td> <td>FNV-1a over the output ids must match the baseline exactly</td> </tr> <tr> <td>common cells only</td> <td>medians are over cells every engine ran and verified</td> </tr> <tr> <td>complete sweep per process</td> <td>each repeat starts in a new process and retains every cell</td> </tr> <tr> <td>physical-core pinning</td> <td>workers are pinned to eight distinct physical cores, never sibling SMT threads</td> </tr> <tr> <td>independent Jobs</td> <td>separate Jobs measure host-to-host variation</td> </tr> </tbody> </table> </div> <p>Repeatedly encoding one document can be faster than encoding a stream of distinct documents on the same build. The first approach measures performance when the entire document is already represented in the cache. The second measures performance on new input while allowing previously seen pre-tokens to remain cached.</p> <p>Both conditions are sometimes described as "warm," even though they measure different workloads. Our headline results use distinct documents, and the complete corpus is too large to fit in the cache. Tokenizer benchmarks should identify which workload they use because the choice can dominate the result.</p> <h2 class="relative group flex items-baseline"> <a id="what-this-adds-up-to" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/tokenizers-v1#what-this-adds-up-to" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> What This Adds Up To </span> </h2> <p>Across the ten model families v1's encode path covers, it encodes text <strong>3 to 30 times faster</strong> than v0.23 with one thread on an Apple M4 Max. The low end is t5-base, the high end gpt2. It scales at <strong>76%</strong> of linear across eight workers. Throughout these changes, v1 produces exactly the same token IDs as the released library.</p> <p>The overall improvement comes from several changes working together: a hand-written splitter in place of a regex engine, a cache that answers a repeated word without merging it again, a merge loop that never touches the allocator, and one model call per batch of pre-tokens instead of one per pre-token. Each reduces the work done at a different point in the pipeline.</p> <p>The next priority is support for more model families. We will move additional models onto the new merge loop before <code>1.0.0</code>. Once the release candidates stabilize, the next step will be bringing about the improvements within the transformers library and the rest of the ecosystem which depend on the tokenizers library.</p> <p>This post is generated from <a href="https://github.com/huggingface/tokbench" rel="nofollow">tokbench</a> results and will be updated as support expands.</p> <h2 class="relative group flex items-baseline"> <a id="getting-it" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/tokenizers-v1#getting-it" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Getting It </span> </h2> <p>A release candidate for v1 is on crates.io. The API you call is the one you already call, so the only thing that changes is which build you install.</p> <p>It is the ordinary install:</p> <pre><code class="language-bash">cargo add tokenizers --pre </code></pre> <p>Training is behind a default-on feature that pulls a C++ dependency with it. If you only need to encode, turn it off to exclude the training implementation:</p> <pre><code class="language-bash">cargo add tokenizers --pre --no-default-features --features http </code></pre> <p>Encoding is unchanged: same call, same ids.</p> <pre><code class="language-rust"><span class="hljs-keyword">use</span> tokenizers::tokenizer::{<span class="hljs-type">Result</span>, Tokenizer}; <span class="hljs-keyword">fn</span> <span class="hljs-title function_">main</span>() <span class="hljs-punctuation">-&gt;</span> <span class="hljs-type">Result</span>&lt;()&gt; { <span class="hljs-keyword">let</span> <span class="hljs-variable">tokenizer</span> = Tokenizer::<span class="hljs-title function_ invoke__">from_pretrained</span>(<span class="hljs-string">"deepseek-ai/DeepSeek-V4-Flash"</span>, <span class="hljs-literal">None</span>)?; <span class="hljs-keyword">let</span> <span class="hljs-variable">encoding</span> = tokenizer.<span class="hljs-title function_ invoke__">encode</span>(<span class="hljs-string">"The tokenizer is no longer the bottleneck."</span>, <span class="hljs-literal">false</span>)?; <span class="hljs-built_in">println!</span>(<span class="hljs-string">"{:?}"</span>, encoding.<span class="hljs-title function_ invoke__">get_ids</span>()); <span class="hljs-comment">// [671, 17840, 9160, 344, 1119, 5827, 270, 111127, 16] println!("{:?}", encoding.get_tokens()); // ["The", "Ġtoken", "izer", "Ġis", "Ġno", "Ġlonger", "Ġthe", "Ġbottleneck", "."]</span> <span class="hljs-title function_ invoke__">Ok</span>(()) } ``` For a batch, `encode_batch` is what scales across cores. It is the call the scaling view above measures. ```rust <span class="hljs-keyword">let</span> <span class="hljs-variable">encodings</span> = tokenizer.<span class="hljs-title function_ invoke__">encode_batch</span>(documents, <span class="hljs-literal">false</span>)?; </code></pre> <p>Every figure in this post was measured against this crate. The Python bindings wrap the same code and are built from <code>bindings/python</code>, but they add per-call overhead that none of these measurements include.</p> <h2 class="relative group flex items-baseline"> <a id="progress-towards-v1" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/tokenizers-v1#progress-towards-v1" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Progress Towards V1 </span> </h2> <p>The benchmarks in this post cover the completed release-candidate work listed first. The remaining sections show what is still required for <code>1.0.0</code> and what we plan to explore afterward.</p> <h3 class="relative group flex items-baseline"> <a id="release-candidate-implemented" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/tokenizers-v1#release-candidate-implemented" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Release Candidate: Implemented </span> </h3> <p>This work is in the Rust pre-release on crates.io:</p> <pre><code class="language-bash">cargo add tokenizers --pre </code></pre> <ul> <li>workspace split: divide the single crate into <code>tk-encode</code>, <code>tk-serialize</code>, <code>tk-convert</code> and <code>tk-train</code>, so an application links only what it uses</li> <li>bitcannon: replace regex splitting on the encoding path with bitstream operations covering GPT-2, cl100k, o200k, Tekken and DeepSeek. This replaced the finite-state machines that shipped first <a href="https://github.com/huggingface/tokenizers/pull/2201" rel="nofollow">#2201</a> <a href="https://github.com/huggingface/tokenizers/pull/2317" rel="nofollow">#2317</a></li> <li>WordCache: reuse the token IDs of previously processed pre-tokens <a href="https://github.com/huggingface/tokenizers/pull/2262" rel="nofollow">#2262</a>, <code>af5a3e3</code></li> <li>faster lookup and merging structures: add FlatCache, MPHF RankStore, incremental merging, and BucketVocabStore <a href="https://github.com/huggingface/tokenizers/pull/2190" rel="nofollow">#2190</a> <a href="https://github.com/huggingface/tokenizers/pull/2188" rel="nofollow">#2188</a></li> <li>reusable model memory: move temporary model state into scratch buffers so tokenization does not allocate on each call <a href="https://github.com/huggingface/tokenizers/pull/2175" rel="nofollow">#2175</a> <a href="https://github.com/huggingface/tokenizers/pull/2183" rel="nofollow">#2183</a></li> <li>pipeline post-processing: expose post-processing as the <code>STAGE_POST</code> pipeline stage <a href="https://github.com/huggingface/tokenizers/pull/2182" rel="nofollow">#2182</a></li> <li>batched model calls: process multiple pre-token spans in one call <a href="https://github.com/huggingface/tokenizers/pull/2304" rel="nofollow">#2304</a></li> <li>faster decoding: write decoded bytes directly into a reusable buffer, avoid intermediate strings and copies, accelerate token lookup, support buffered streaming, and decode batches in parallel</li> <li><code>role_to_token</code> support <a href="https://github.com/huggingface/tokenizers/pull/2343" rel="nofollow">#2343</a></li> <li>Node.js bindings <a href="https://github.com/huggingface/tokenizers/pull/2281" rel="nofollow">#2281</a></li> </ul> <h3 class="relative group flex items-baseline"> <a id="100" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/tokenizers-v1#100" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> 1.0.0 </span> </h3> <ul> <li>one encoding implementation: use <code>tk-encode</code> during training validation so training and inference cannot produce different tokenization results</li> <li>optional offsets and masks: compute this metadata only when requested, keeping it off the token-ID-only path</li> <li>rework normalizers</li> <li>bitnorm support, building on atomnorm <a href="https://github.com/huggingface/tokenizers/pull/2209" rel="nofollow">#2209</a></li> <li>spm precompiled</li> <li>simpler Python bindings: reduce locking, wrapper types, and handwritten dispatch code while preserving subclassing, serialization, custom decoders, mutation behavior, and support for free-threaded CPython</li> <li>inference-only C and C++ bindings for ExecuTorch and llama.cpp, with possible JVM, Swift, and Go bindings to follow</li> </ul> <h3 class="relative group flex items-baseline"> <a id="after-100" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/tokenizers-v1#after-100" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> After 1.0.0 </span> </h3> <ul> <li>tok-devices: explore GPU encoding and batch decoding while keeping text and token IDs on the device. The decoder would upload the vocabulary once, calculate output positions in parallel, and gather the corresponding bytes on the GPU. This would be an optional component intended for large batches, subject to further prototyping and measurement.</li> </ul> <!----></div>

2026/9/21
阅读更多

Your Agent Aced the Task. Will It Do It Again?

<div class="SVELTE_HYDRATER contents" data-target="RepoCodeCopy" data-props="{}"><div></div></div> <div> </div> <div class="relative overflow-clip [overflow-clip-margin:3rem] [&amp;>:last-child]:mb-0"> <p></p>Your agent works in rehearsal, but during the live demo, it takes a different path and fails the same task.<p></p> <p>That is embarrassing onstage. In production, it is a reliability problem: a workflow that succeeded once may fail the next time a user makes the same request. For mission-critical work, such as reconciling a financial transaction or checking a contract for an obligation, that can be a showstopper.</p> <p>Most benchmarks hide this variability behind an average. On AppWorld, a ReAct agent using GPT-4.1 succeeded on <strong>77.4% of runs</strong> across five repetitions. But it succeeded in all five runs for only <strong>53.0% of tasks</strong> — a <strong>24.4-point consistency gap</strong>.</p> <p>Most benchmarks report the first number. We built a way to measure the second — and improve it.</p> <p>In an earlier post, we introduced <a href="https://huggingface.co/blog/ibm-research/altk-evolve">ALTK-Evolve</a> — a system that turns an agent's own past trajectories into reusable guidelines, distilled automatically and injected back at inference time. It measurably improves task success, but those results only asked the average-case question too. This post introduces <strong>consistency guidelines</strong>, a new guideline type in <a href="https://github.com/AgentToolkit/altk-evolve" rel="nofollow"><code>altk-evolve</code></a> built on top of a diagnostic tool we call the <strong>Consistency Analyzer</strong>, that targets this gap directly. </p> <p><strong>TL;DR</strong></p> <ul> <li><strong>Accuracy hides an unreliability problem.</strong> A ReAct agent (GPT-4.1 on AppWorld <code>test_normal</code>) that succeeds 77.4% of the time on average succeeds on <em>all 5</em> repeated runs for only 53.0% of tasks — a <strong>24.4-point consistency gap</strong>. On hard tasks it reaches 30 points.</li> <li><strong>We built a diagnostic for exactly this.</strong> The Consistency Analyzer resamples an agent's own recorded trajectory to find <em>flip-prone</em> decision points — steps where the model was one token-sample away from doing something different. It needs one trace and no ground truth — it resamples each decision point in that trace with a single call requesting k completions (k=5 by default), rather than re-running the task end-to-end.</li> <li><strong>Turning that diagnosis into guidelines halves the gap</strong> — from <strong>24.4pp to 12.0pp</strong> (same-task Pass⁵ +16.0pp, similar-task +13.0pp), without costing anything in average accuracy.</li> <li><strong>Full methodology and evaluations</strong> are in the <a href="https://arxiv.org/abs/2609.08832" rel="nofollow">technical report on arXiv</a>.</li> </ul> <hr> <h2 class="relative group flex items-baseline"> <a id="the-metric-almost-nobody-reports" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/ibm-research/altk-evolve-consistency#the-metric-almost-nobody-reports" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> The Metric Almost Nobody Reports </span> </h2> <p>Standard agent evaluation reports <strong>Mean@k</strong>: run a benchmark <em>k</em> times, average the pass rate. Often <em>k</em>=3, sometimes just 1. It's the number on every leaderboard, and it's what "77% accurate" means in practice.</p> <p>Mean@k answers "how good is this agent, on average?" It does not answer the question a real user cares about: <strong>will it still be good if I ask this exact question again?</strong> For that you need <strong>Pass^k</strong>: the fraction of tasks where the agent succeeds on <em>all</em> k runs.</p> <blockquote> <p>⚠️ <strong>Pass^k is not Pass@k.</strong> The familiar <strong>Pass@k</strong> is <em>optimistic</em> — it asks whether <strong>at least one</strong> of k attempts succeeded, the right question when you can verify and retry. <strong>Pass^k</strong> is its pessimistic mirror image: <strong>every</strong> attempt must succeed. Same letters, opposite question. Pass^k ≤ Mean@k ≤ Pass@k, always.</p> </blockquote> <div align="center"> <img alt="Mean@5 vs. Pass^5 by task difficulty, GPT-4.1 on AppWorld test_normal, with the consistency gap called out in red" src="https://imgproxy.citrusreader.com/qq8GFOgCVLnruTPZ_r5qEBtJYu6Z5JXgToXErskvdvw/raw:1/aHR0cHM6Ly9jZG4tdXBsb2Fkcy5odWdnaW5nZmFjZS5jby9wcm9kdWN0aW9uL3VwbG9hZHMvNjQzNWExMTMxODYwMDAxZjE0NDIzOWVhL0Jkem1TZ2tvbjFkQXRiT0NXcW1LRi5wbmc" width="500" referrerpolicy="no-referrer"> </div> <p>A ReAct agent backed by GPT-4.1 posts a Mean@5 of <strong>77.4%</strong> — genuinely strong. But Pass^5 is only <strong>53.0%</strong>. Nearly a quarter of the benchmark consists of tasks the agent can <em>sometimes</em> solve and sometimes can't, with nothing about the task changing between runs. We call this gap — Mean@k minus Pass^k — the <strong>consistency gap</strong>.</p> <p>This isn't a capability problem you fix with a bigger model. It's an orthogonal axis: <strong>an agent can be capable and inconsistent at the same time.</strong></p> <hr> <h2 class="relative group flex items-baseline"> <a id="why-agents-flip-sharp-decisions-vs-flat-ones" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/ibm-research/altk-evolve-consistency#why-agents-flip-sharp-decisions-vs-flat-ones" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Why Agents Flip: Sharp Decisions vs. Flat Ones </span> </h2> <p>Every time an LLM agent decides something — which API to call, what argument to pass, whether to retry — that decision comes out of a probability distribution over next tokens. What matters is the <strong>shape</strong> of that distribution. A <strong>sharp</strong> one puts most of its mass on a single token: the runners-up are far behind, and the same choice comes out run after run. A <strong>flat</strong> one spreads comparable mass across several near-tied tokens, and which one wins is close to a coin flip.</p> <p>The shape decides how much noise it takes to change the outcome. Sharp distributions are resilient — GPU floating-point non-associativity, request batching, and other platform-side effects nudge the numbers slightly, but nowhere near enough to reorder a clear winner. Flat distributions are vulnerable to exactly that nudge: near-ties may reorder under small perturbations. And because a trajectory chains dozens of decisions, a small per-step chance of flipping compounds into a large chance that <em>some</em> run goes differently. That's where a 24-point gap comes from.</p> <p>This is also why the problem survives your decoding settings. Greedy decoding and a fixed seed both govern <em>how a distribution gets turned into a token</em> — they say nothing about the distribution itself. On a hosted endpoint the probabilities shift slightly from run to run, so the same prompt to the same model at temperature zero can still resolve a near-tie one way today and the other way tomorrow. </p> <blockquote> <p><strong>Our setup:</strong> the ReAct agent runs at <strong>temperature 0.0</strong>, so none of the variance above is ordinary sampling.</p> </blockquote> <hr> <h2 class="relative group flex items-baseline"> <a id="diagnose-then-fix" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/ibm-research/altk-evolve-consistency#diagnose-then-fix" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Diagnose, Then Fix </span> </h2> <p>Which turns the problem into a search: which steps in a given trajectory were the flat ones — and what do you do about them once you know?</p> <p>Consistency guidelines come out of a two-stage pipeline that plugs into ALTK-Evolve's existing machinery — with a new source signal driving what gets written.</p> <div align="center"> <img alt="consistency-guideline-pipeline" src="https://imgproxy.citrusreader.com/PnyFn9Y_i-oejoKPs3g4J_LitqnEa5ve8tDdA2m-dMY/raw:1/aHR0cHM6Ly9jZG4tdXBsb2Fkcy5odWdnaW5nZmFjZS5jby9wcm9kdWN0aW9uL3VwbG9hZHMvNjQzNWExMTMxODYwMDAxZjE0NDIzOWVhL2dkVWQ3bDZROER6WENnem1jaHhpMy5wbmc" width="680" referrerpolicy="no-referrer"> </div> <p><strong>1. Detect — the Consistency Analyzer.</strong> Given one recorded trajectory, the analyzer replays each decision step through controlled resampling, measuring how much the model's output actually varies at that point. Concretely, that's one additional model call per decision step, done once offline — issued with the sampling parameter set to draw k completions at once (k=5 by default) — replayed against the already-recorded context, not new tool calls, not new environment interactions, and not a second end-to-end rollout of the task. This yields a consistency score per decision step that is written into a scorecard to pinpoint exactly which decisions are at risk of flipping on the next run. Detection is fully black-box — no logits, no model internals, no instrumentation beyond the trace you already have.</p> <p><strong>2. Generate — targeted guidelines.</strong> Every flagged step becomes a candidate consistency guideline in the standard ALTK-Evolve format, so it slots into the existing storage and retrieval pipeline. Here's a real example, generated by GPT-4.1 from a trajectory of the AppWorld task "How many activities are done in my bucket list as per my SimpleNote note?":</p> <blockquote> <p>[Guideline 1] When counting checkbox-style markers in note content, use a line-anchored regex match rather than a plain substring count — note titles often repeat the marker symbol in a legend line.</p> <p>[Guideline 2] Always verify search results for note queries by checking for multiple matches and confirming the correct note before proceeding.</p> </blockquote> <p>Nothing here is task-specific trivia. String-counting bugs and unverified search results are decision points that show up with high uncertainty across many AppWorld tasks. That's the point: the analyzer targets instability, not failure — so it catches steps the agent happened to get right this time but could easily get wrong next time.</p> <p><a href="https://www.youtube.com/watch?v=qlp7EzXe8Pg" rel="nofollow">Watch the 2-minute demo</a> — five parallel runs of the agent split 3-2 on this task because of agent uncertainty about the counting strategy, then run again after these guidelines in context: all five agree.</p> <hr> <h2 class="relative group flex items-baseline"> <a id="results-reducing-the-gap-without-losing-accuracy" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/ibm-research/altk-evolve-consistency#results-reducing-the-gap-without-losing-accuracy" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Results: Reducing the Gap Without Losing Accuracy </span> </h2> <p>We evaluated on AppWorld <code>test_normal</code> (168 tasks) with a ReAct agent on GPT-4.1, generating consistency guidelines from a single baseline trajectory per task and testing them on 5 fresh runs.</p> <div align="center"> <img alt="Pass⁵ lift from baseline to consistency guidelines, by task difficulty, GPT-4.1 on AppWorld test_normal" src="https://imgproxy.citrusreader.com/st0w6J5WxwZxAlO17G8bBB4Isp2x0OEhkXACC5NfAAE/raw:1/aHR0cHM6Ly9jZG4tdXBsb2Fkcy5odWdnaW5nZmFjZS5jby9wcm9kdWN0aW9uL3VwbG9hZHMvNjQzNWExMTMxODYwMDAxZjE0NDIzOWVhL0VfN0h5QXQ1QlN6Wnk5UGowazNXYy5wbmc" width="680" referrerpolicy="no-referrer"> </div> <div align="center"> <img alt="Mean@5 aggregate, baseline vs. consistency guidelines, same scale as the Pass⁵ chart above" src="https://imgproxy.citrusreader.com/AA12-AMSTU8RWxI-TXU_hdZrR0J6geDmFaTsRycA3s0/raw:1/aHR0cHM6Ly9jZG4tdXBsb2Fkcy5odWdnaW5nZmFjZS5jby9wcm9kdWN0aW9uL3VwbG9hZHMvNjQzNWExMTMxODYwMDAxZjE0NDIzOWVhL2p6eFNVNU1KOEFqZjIxYUVqQU55Qi5wbmc" width="680" referrerpolicy="no-referrer"> </div> <p align="center"><sub>Mean@5 (%), aggregate — same scale as Pass^5 above.</sub></p> <p><strong>The consistency gap is cut roughly in half.</strong> Aggregate Pass^5 rises 53.0% → 69.0% while Mean@5 rises 77.4% → 81.0%, narrowing the gap between "looks capable" and "can be counted on" from <strong>24.4pp to 12.0pp</strong>. Nearly a third of previously-inconsistent tasks become tasks the agent passes on <em>every single run</em>.</p> <p><strong>The middle and hard tiers gain most.</strong> Medium +22.9pp (+44% relative), Hard +14.3pp (+45% relative) — effectively tied in relative terms, with Medium ahead absolutely. Easy gains +12.2pp, having had the least room. This is consistency guidelines doing what they're designed to do: finding and stabilizing the specific decision points where an agent's own uncertainty was leaking into the outcome.</p> <p><strong>Mean@5 never drops.</strong> Preserving average accuracy was a hard requirement, not a nice-to-have: a system that boosts Pass^5 by trading away Mean@5 would just be shifting unreliability around, not fixing it. Mean accuracy holds or improves at every difficulty level.</p> <h3 class="relative group flex items-baseline"> <a id="the-guidelines-generalize--they-arent-patching-one-trajectory" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/ibm-research/altk-evolve-consistency#the-guidelines-generalize--they-arent-patching-one-trajectory" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> The guidelines generalize — they aren't patching one trajectory </span> </h3> <p>Applied to a <strong>different but related task in the same AppWorld scenario</strong> — another variant of the scenario the guidelines were mined from — consistency guidelines still lift Pass^5 by <strong>+13.0pp</strong>, only 3 points below the same-task number. A guideline derived from one run isn't just patching that run; it's capturing something that transfers.</p> <p>The sharper evidence comes from a weaker model, <strong>gpt-oss-120b</strong>. Same-task Pass^5 rose +6.0pp from a much lower baseline (10.1% → 16.1%) — and, interestingly, the similar-task generalization number (+8.7 pp) actually <em>exceeded</em> the same-task gain, suggesting the guidelines were capturing genuinely reusable failure patterns rather than memorizing one trajectory's specifics.</p> <hr> <h2 class="relative group flex items-baseline"> <a id="if-youre-shipping-an-agent" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/ibm-research/altk-evolve-consistency#if-youre-shipping-an-agent" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> If You're Shipping an Agent </span> </h2> <ul> <li><strong>Report Pass^k next to Mean@k.</strong> Averages can't distinguish a reliable agent from a lucky one; even k=3 will surface a gap you didn't know you had.</li> <li><strong>Expect the gap to widen with difficulty.</strong> Your hardest tier is where a single averaged number is most misleading.</li> <li><strong>Don't reach for a bigger model first.</strong> Consistency is orthogonal to capability. A stronger model raises Mean@k; it doesn't necessarily reduce the consistency gap.</li> <li><strong>Diagnosis needs no grader and no live replay</strong>. One extra LLM call per decision step (sampling k=5 completions by default) is enough — no ground truth, no re-running the task against the environment. That's what makes it usable on production traffic, where you often can't replay a task end-to-end even once.</li> </ul> <hr> <h2 class="relative group flex items-baseline"> <a id="try-it" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/ibm-research/altk-evolve-consistency#try-it" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Try It </span> </h2> <blockquote> <p><strong>Try the <a href="https://github.com/AgentToolkit/altk-evolve" rel="nofollow">ALTK-Evolve</a></strong> — the open-source repo now includes the Consistency Analyzer and consistency-guideline generation used in these experiments — <strong>or read the <a href="https://arxiv.org/abs/2609.08832" rel="nofollow">technical report on arXiv</a></strong> for the complete methodology.</p> </blockquote> <p>If accuracy numbers you can't reproduce on your own tasks sound familiar, we'd like to hear about it — concrete examples of flip-prone behavior in your own agents are exactly the kind of feedback that shapes what we build next. <a href="https://github.com/AgentToolkit/altk-evolve" rel="nofollow">Open an issue or a discussion</a>.</p> <hr> <h2 class="relative group flex items-baseline"> <a id="appendix-understanding-the-metrics" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/ibm-research/altk-evolve-consistency#appendix-understanding-the-metrics" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Appendix: Understanding the Metrics </span> </h2> <ul> <li><strong>Mean@k.</strong> Run a task <em>k</em> times, report the average pass rate — what most benchmarks call "accuracy."</li> <li><strong>Pass^k.</strong> The fraction of tasks where the agent succeeds on <strong>all</strong> <em>k</em> independent runs. Always ≤ Mean@k. What a user experiences if they run the same query twice.</li> <li><strong>Pass@k</strong> At least <strong>one</strong> of <em>k</em> runs succeeds — the optimistic counterpart, common in code-generation papers.</li> <li><strong>Consistency gap.</strong> Mean@k − Pass^k, in percentage points.</li> </ul> <hr> <h2 class="relative group flex items-baseline"> <a id="linked-artifacts--references" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/ibm-research/altk-evolve-consistency#linked-artifacts--references" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Linked artifacts / references </span> </h2> <ul> <li><strong>ALTK-Evolve open source repo</strong> — <a href="https://github.com/AgentToolkit/altk-evolve" rel="nofollow">github.com/AgentToolkit/altk-evolve</a></li> <li><strong>Technical report</strong> — <a href="https://arxiv.org/abs/2609.08832" rel="nofollow">arXiv</a></li> <li><strong>Consistency guideline demo</strong> —<a href="https://www.youtube.com/watch?v=qlp7EzXe8Pg" rel="nofollow">2 min video</a></li> </ul> <hr> </div>

2026/9/15
阅读更多

Async GRPO with LoRA across HF Jobs: a bucket, a proxy, and no NCCL

<div class="SVELTE_HYDRATER contents" data-target="RepoCodeCopy" data-props="{}"><div></div></div> <div> <a target="_blank" class="btn mb-5 font-sans text-sm no-underline" href="https://github.com/huggingface/blog/blob/main/asyncgrpo-lora-hfjobs.md">Update on GitHub</a></div> <div class="relative overflow-clip [overflow-clip-margin:3rem] [&amp;>:last-child]:mb-0"> <blockquote> <p></p><strong>TL;DR</strong><p></p> <ul> <li><code>AsyncGRPOTrainer</code> can now train a LoRA adapter and sync only that adapter to vLLM (TRL v1.14).</li> <li>A rank-1 adapter is a few megabytes, so it can travel through a Storage Bucket mounted in every Job instead of over NCCL. The trainer and the vLLM replicas run as separate Hugging Face Jobs on separate machines.</li> <li>A small proxy in front of the replicas adds the auth header, routes each rollout to the replica that already holds its KV prefix, and broadcasts adapter loads to every replica.</li> <li>The AsyncGRPO metrics show where the bottleneck sits. Five runs take the same recipe from 3 h 27 min to 53 min for 500 steps.</li> </ul> </blockquote> <p>LoRA support recently landed in TRL's <a href="https://huggingface.co/docs/trl/en/async_grpo_trainer"><code>AsyncGRPOTrainer</code></a> with <a href="https://github.com/huggingface/trl/pull/7017" rel="nofollow">PR #7017</a>, and ships with TRL v1.14. The asynchronous trainer can now train an adapter instead of the full model, and it syncs only the LoRA adapter to vLLM. This post covers a real-world project built on top of it, where training and inference no longer share a machine.</p> <p>LoRA training is particularly suited for RL, as shown in Thinking Machines's blog <a href="https://thinkingmachines.ai/blog/lora/" rel="nofollow">LoRA Without Regret</a>. They show that LoRA can match full fine-tuning for policy-gradient RL, even with rank 1. This stems from the fact that the advantage function only gives <code>~O(1)</code> bits of information per episode, so there is not that much to learn from each step, from a total-bits-of-information point of view. A rank-1 adapter has enough capacity to absorb it.</p> <p>There is also a systems consequence of LoRA training. A rank-1 adapter for a 1.5B model is a few megabytes, while the full model is around 3 GB. Instead of sending the full policy to the inference workers after every update, we can just send the adapter. vLLM can also keep several adapters loaded at once. Old rollouts finish with the policy they started with, while new rollouts use the latest one.</p> <p>TRL's <code>AsyncGRPOTrainer</code> already separates training and generation. The trainer and vLLM can run on different machines and at their own speed. This is easy in a single-node or cluster setting where both processes share a filesystem or can form an NCCL group.</p> <p>What we want is to run the same setup with <a href="https://huggingface.co/docs/huggingface_hub/guides/jobs">Hugging Face Jobs</a>. Essentially, an HF Job is one container running on one VM. This means that one Job cannot spawn multiple nodes (at least for now) to hold a trainer and a fleet of vLLM servers (we are limited to 8xH200 at most per node). The <code>AsyncGRPOTrainer</code> is built for exactly that kind of scale, so the question became: how far can we get if we drop the requirement that the trainer and the inference servers share a node?</p> <p>Well, with a full-weight sync, the answer would be "not far". Every update would have to move gigabytes between machines, which is what NCCL is for in a dense cluster, but Jobs can't communicate <em>across</em> nodes. There is no shared local disk and obviously no shared <code>localhost</code>. With LoRA, a sync is only a few megabytes. For the filesystem part, HF Jobs provide volumes backed by <a href="https://huggingface.co/docs/hub/storage-buckets">Storage Buckets</a>! These buckets can then be mounted as a FUSE filesystem in every Job and are enough to work as a shared FS between nodes. No network path between the Jobs is needed at all.</p> <p>The setup ended up being quite small:</p> <ul> <li>a <strong>trainer Job</strong> running <code>AsyncGRPOTrainer</code> with LoRA (and FSDP, more on that later),</li> <li><strong>two vLLM Jobs</strong>, each serving the base model plus whatever adapter the trainer last published,</li> <li>a <strong>Storage Bucket</strong> mounted in all three at the same path, which is how the adapter gets from the trainer to the servers,</li> <li>a <strong>proxy server</strong>. We'll dive deeper into why we need one, but at a high level we need a proxy that routes each rollout to the replica most likely to hold its KV cache, and broadcasts every adapter update to all vLLM replicas.</li> </ul> <h2 class="relative group flex items-baseline"> <a id="the-architecture-leveraging-hugging-face-jobs-and-storage-buckets-🪣" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#the-architecture-leveraging-hugging-face-jobs-and-storage-buckets-%F0%9F%AA%A3" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> The architecture: leveraging Hugging Face Jobs and Storage Buckets 🪣 </span> </h2> <p>The new adapter-only sync path in <code>AsyncGRPOTrainer</code> works like this. The trainer does not send tensors to vLLM. Every few optimizer steps, it saves the adapter under <code>&lt;output_dir&gt;/.vllm_lora/trl-policy-v{N}</code>, publishes the directory with an atomic rename, then sends its path to vLLM's <code>/v1/load_lora_adapter</code> endpoint. vLLM loads the files from disk, so the rollout worker can then request <code>model="trl-policy-v{N}"</code>.</p> <p>This is how runtime adapter loading already works in vLLM. The endpoint takes a path, not tensors, so the trainer and the server are expected to share a filesystem. On a Slurm cluster, that is the network filesystem. On Jobs, we get the same thing by mounting a <a href="https://huggingface.co/docs/hub/storage-buckets">Storage Bucket</a> as a volume at the same path in every Job, as we mentioned earlier. Under the hood, it uses <a href="https://github.com/huggingface/hf-mount" rel="nofollow"><code>hf-mount</code></a>, which exposes the bucket as a POSIX filesystem inside the container:</p> <pre><code class="language-sh"><span class="hljs-comment"># every Job gets the same bucket at the same absolute path</span> hf <span class="hljs-built_in">jobs</span> run ... -v hf://buckets/aminediroHF/asyncgrpo-lora-buckets:/lora ... </code></pre> <p>Nothing in TRL or vLLM had to change for this. The trainer writes to <code>/lora/&lt;run&gt;/.vllm_lora/</code> and the servers read from the same path. The path sent in the POST request is already valid inside every container.</p> <figure class="image text-center"> <img src="https://imgproxy.citrusreader.com/-ypDAZUDB87CT_EIoOjxyHzrGl7dSt4Ns7qfg_70iug/raw:1/aHR0cHM6Ly9odWdnaW5nZmFjZS5jby9kYXRhc2V0cy9odWdnaW5nZmFjZS9kb2N1bWVudGF0aW9uLWltYWdlcy9yZXNvbHZlL21haW4vYmxvZy9hc3luY2dycG8tbG9yYS1oZmpvYnMvYXJjaGl0ZWN0dXJlLnBuZw" alt="Async GRPO with LoRA across Hugging Face Jobs. The trainer Job runs AsyncGRPOTrainer and the proxy, two vLLM Jobs serve the base model plus the latest adapter, and a Storage Bucket is mounted at /lora in all three." referrerpolicy="no-referrer"> <figcaption>The three Jobs and the bucket. TRL talks to the proxy over localhost, the proxy talks to the replicas over HTTPS, and the adapter directory travels through the bucket mount.</figcaption> </figure> <p>Note that we also store the checkpoints and the final adapter in the bucket. The HF Jobs are ephemeral, but a preempted trainer can resume training, as the final adapter is always persisted to the bucket and is never lost when the Job stops.</p> <h2 class="relative group flex items-baseline"> <a id="the-three-jobs" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#the-three-jobs" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> The three Jobs </span> </h2> <h3 class="relative group flex items-baseline"> <a id="the-vllm-replicas" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#the-vllm-replicas" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> The vLLM replicas </span> </h3> <p>Each replica uses one GPU and the stock <code>vllm/vllm-openai</code> image. We only need to enable <a href="https://docs.vllm.ai/en/latest/features/lora/?h=lora#serving-lora-adapters" rel="nofollow">runtime LoRA</a> loading and reserve enough adapter slots.</p> <p>The number of adapter slots follows from <code>max_staleness</code>. In <code>AsyncGRPOTrainer</code>, every weight sync bumps the policy version by one, and <code>max_staleness</code> is how many versions a rollout sample may lag behind the current policy before the trainer discards it. With <code>max_staleness=4</code>, a sample generated under <code>trl-policy-v3</code> is still used for training while the trainer is at <code>v7</code>. A rollout that started under <code>v3</code> must also be able to finish under <code>v3</code>. So at any moment, vLLM has to serve the current policy plus the four before it. That is why the trainer keeps <code>max_staleness + 1</code> adapter versions registered and unloads anything older. Each sync loads the new version before it unloads the oldest one, which needs one more slot during the swap. That gives <code>--max-loras 6</code>. With only five, vLLM would silently evict a policy that still has rollouts in flight at every sync.</p> <pre><code class="language-sh"><span class="hljs-comment"># --expose 8000 reachable at https://&lt;job_id&gt;--8000.hf.jobs</span> <span class="hljs-comment"># -v ...:/lora:ro read-only: the server only reads adapters</span> <span class="hljs-comment"># VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 enables /v1/load_lora_adapter</span> <span class="hljs-comment"># VLLM_SERVER_DEV_MODE=1 enables /pause, /resume, /server_info (TRL needs all three)</span> <span class="hljs-comment"># --max-loras 6 max_staleness=4 -&gt; 4+2 adapter slots</span> <span class="hljs-keyword">for</span> replica <span class="hljs-keyword">in</span> 1 2; <span class="hljs-keyword">do</span> hf <span class="hljs-built_in">jobs</span> run --detach --flavor h200 --<span class="hljs-built_in">timeout</span> 8h --secrets HF_TOKEN \ --expose 8000 \ -v <span class="hljs-string">"hf://buckets/<span class="hljs-variable">${BUCKET}</span>:/lora:ro"</span> \ -e VLLM_ALLOW_RUNTIME_LORA_UPDATING=1 \ -e VLLM_SERVER_DEV_MODE=1 \ -- vllm/vllm-openai:v0.27.1 \ vllm serve Qwen/Qwen2.5-Math-1.5B --host 0.0.0.0 --port 8000 \ --max-model-len 4096 --logprobs-mode processed_logprobs --generation-config vllm \ --enable-lora --max-lora-rank 1 --max-loras 6 <span class="hljs-keyword">done</span> </code></pre> <p>We pin vLLM to <code>v0.27.1</code>. vLLM moves fast, and the flags above and the runtime LoRA endpoints are the ones that version exposes, so treat the version as part of the recipe.</p> <p>There is another possible design where the trainer keeps only the latest adapter and always publishes it under the same name. We did not go that way, because vLLM keys its prefix cache by adapter name. With a single name, KV blocks computed under the previous weights would still match after the swap, so the prefill would not be redone and a rollout could get its prefix from one policy version and its decode from the next. The trainer would have no way to tell, and it would show up as <code>ratio</code> drifting away from 1. Versioned names make this impossible: a name always means one set of weights, and a cached prefix can never match a newer version.</p> <h3 class="relative group flex items-baseline"> <a id="the-dataset-choice-the-sanity-set" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#the-dataset-choice-the-sanity-set" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> The dataset choice: the Sanity set </span> </h3> <p>We chose <a href="https://huggingface.co/datasets/sail/Sanity-Test-R1D-1.5B"><code>sail/Sanity-Test-R1D-1.5B</code></a>, the dataset from <a href="https://huggingface.co/papers/2510.26788">Defeating the Training-Inference Mismatch via FP16</a> (Qi et al., 2025). The reproduction code is in <a href="https://github.com/sail-sg/Precision-RL" rel="nofollow"><code>sail-sg/Precision-RL</code></a>.</p> <p>The authors generated 40 answers for each MATH problem with <a href="https://huggingface.co/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B">DeepSeek-R1-Distill-Qwen-1.5B</a>. They kept problems with a success rate between 20% and 80%, yielding 1,460 questions. This dataset is really good for RL validation because the questions are neither already solved nor completely hopeless for that model, meaning the model can get a good early signal to train on and improve.</p> <p>This is awesome as a robust end-to-end test: if one vLLM replica silently serves the base model under an adapter name, we want to see that in the curve within a few dozen steps. Also, this dataset is small enough to cycle through in less than two hours.</p> <p>We also take the hyperparameters from the paper's LoRA scripts in <a href="https://github.com/sail-sg/Precision-RL/tree/main/oat/scripts/lora" rel="nofollow"><code>oat/scripts/lora</code></a>: <code>Qwen/Qwen2.5-Math-1.5B</code>, LoRA rank 1 with alpha 2, a learning rate of 4e-5, 8 samples per prompt, 128 completions per step, a maximum of 3,000 generated tokens and a 4,096-token context.</p> <h3 class="relative group flex items-baseline"> <a id="the-trainer" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#the-trainer" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> The trainer </span> </h3> <p>The trainer uses the same <code>vllm/vllm-openai:v0.27.1</code> image with TRL installed on top. We ran the PR branch at the time; the same code now ships in TRL v1.14. The training script is a normal <code>AsyncGRPOTrainer</code> script. The only Job-specific values are the output directory and the server URL.</p> <pre><code class="language-python"><span class="hljs-keyword">from</span> peft <span class="hljs-keyword">import</span> LoraConfig <span class="hljs-keyword">from</span> trl.experimental.async_grpo <span class="hljs-keyword">import</span> AsyncGRPOConfig, AsyncGRPOTrainer config = AsyncGRPOConfig( output_dir=<span class="hljs-string">"/lora/sanity-lora-r1"</span>, <span class="hljs-comment"># on the bucket: adapters, checkpoints and the final adapter all land here</span> vllm_server_base_url=<span class="hljs-string">"http://localhost:8000"</span>, <span class="hljs-comment"># the proxy, not a vLLM Job; TRL never sees the Jobs URLs</span> max_staleness=<span class="hljs-number">4</span>, weight_sync_steps=<span class="hljs-number">4</span>, <span class="hljs-comment"># publish an adapter every 4 optimizer steps</span> save_strategy=<span class="hljs-string">"steps"</span>, save_steps=<span class="hljs-number">50</span>, <span class="hljs-comment"># checkpoints go to the same bucket -&gt; resume after preemption</span> ... ) trainer = AsyncGRPOTrainer( model=<span class="hljs-string">"Qwen/Qwen2.5-Math-1.5B"</span>, args=config, peft_config=LoraConfig(r=<span class="hljs-number">1</span>, lora_alpha=<span class="hljs-number">2</span>, target_modules=<span class="hljs-string">"all-linear"</span>), <span class="hljs-comment"># plain LoRA vLLM can serve as-is</span> ... ) </code></pre> <blockquote class="note"> <p>During initialization, TRL calls <code>/server_info</code>. If it finds a <code>lora_config</code>, it uses adapter-only sync. Configurations vLLM cannot serve directly, such as DoRA, <code>modules_to_save</code>, or a rank above <code>--max-lora-rank</code>, fall back to merged-weight sync with a warning. The log should contain <code>Adapter-only vLLM sync enabled</code>.</p> </blockquote> <h2 class="relative group flex items-baseline"> <a id="the-proxy" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#the-proxy" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> The proxy </span> </h2> <p>Now onto the fun stuff. We need a proxy between the trainer and the vLLM Jobs for two reasons:</p> <ol> <li><p>Exposed Job ports require an <code>Authorization: Bearer &lt;HF token&gt;</code> header on every request. The proxy is where that header gets added, so TRL does not need to know about it.</p> </li> <li><p>We want more than one GPU generating. On a single vLLM server, the usual way to get that is <code>--data-parallel-size &gt; 1</code>, but TRL refuses adapter-only sync in that mode, for a good reason: a call to <code>/v1/load_lora_adapter</code> only reaches the DP rank that answers it, so the other ranks would keep serving the base model under the new policy name. On Jobs the question does not even arise, since each replica is its own machine. So the data parallelism has to live one level up, in something that fans the adapter load out to every replica.</p> </li> </ol> <p>We therefore run a small proxy at <code>127.0.0.1:8000</code> on the trainer Job and point TRL to it as if it were a single vLLM server. Besides adding the header, the proxy does two things functionally:</p> <ul> <li>It sends each completion request to one replica, chosen so that the eight rollouts of a prompt land where their prefix is already cached (details on this below).</li> <li>It broadcasts every <em>state-changing</em> request, such as <strong>adapter loads, pause and resume</strong>, to all replicas, so that a policy name means the same thing everywhere.</li> </ul> <h3 class="relative group flex items-baseline"> <a id="routing-rollouts-by-kv-prefix" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#routing-rollouts-by-kv-prefix" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Routing rollouts by KV prefix </span> </h3> <p>A quick reminder of why this matters. Generating a completion has two phases with very different workload profiles:</p> <ul> <li>The prefill processes the <strong>whole prompt at once</strong> and computes the attention keys and values for every prompt token.</li> <li>The decode phase then produces one token at a time, and each new token attends to the keys and values of all the tokens before it.</li> </ul> <p>Those keys and values are the KV cache. Because attention is causal, the KV of a token depends only on the tokens before it, not on what comes after. Two requests that share a prefix therefore share the KV of that prefix, and a replica that already has it in cache can <strong>skip that part of the prefill entirely</strong>. The whole game now is to find that replica, so a request can benefit from landing on the replica that has already seen its prefix.</p> <p>vLLM stores its <a href="https://github.com/vllm-project/vllm/blob/main/vllm/v1/core/kv_cache_utils.py" rel="nofollow">prefix KV cache</a> in blocks of 16 tokens. Because of GRPO, the rollout worker sends <code>G</code> requests with the same prompt (in our case <code>G=8</code>). If they all reach the same replica, the first request computes the prefill and the next seven reuse it. With round-robin routing, half would go to a replica that doesn't have the prefix cached, and those four requests would redo the prefill work and waste valuable GPU compute.</p> <p>The job of <a href="https://github.com/AmineDiro/hfjobs-lora-buckets/blob/main/src/lora_proxy.py" rel="nofollow">our router</a> is to track which replica has seen which <strong>block hash</strong>. One important detail is that the hashes are chained, so the hash of block 3 represents blocks 1, 2 and 3, not just block 3. This mirrors causal attention: the KV of block 3 is only valid if blocks 1 and 2 are the same too. We also seed the chain with the adapter name because the KV cache also depends on the adapter that generated it: a prefix cached for policy v3 is useless for policy v4!</p> <figure class="image text-center"> <video controls="" autoplay="" loop="" muted="" playsinline="" style="max-width: 100%; margin: auto;"> <source src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/asyncgrpo-lora-hfjobs/kv-prefix-router.mp4" type="video/mp4"> Your browser does not support the video tag. </video> <figcaption>The routing decision for two prompts and four requests on two replicas: 16-token blocks, chained hashes, the common prefix, one affinity hit and one spill.</figcaption> </figure> <p>The video walks through the entire decision process for choosing a replica. The steps below go through a real 135-token completion request example (from the Sanity dataset problems):</p> <p><strong>1. Split the prompt into blocks.</strong> The router receives token ids and cuts them into 16-token blocks, just like vLLM. It only hashes complete blocks, so the last 7 tokens are ignored here.</p> <p><strong>2. Hash the prefix.</strong> Each block is hashed with the previous hash, starting from the adapter seed. <code>h3</code> therefore identifies blocks 1, 2 and 3 in order. Two prompts with the same first <code>k</code> blocks get the same hashes up to <code>hk</code>. Once one block changes, every hash after it changes too. This is why we seed the hashes with the adapter name: the same prompt under <code>trl-policy-v4</code> starts from another seed and cannot match entries from <code>v3</code>. This is what we want because the old KV blocks were computed with different weights.</p> <p><strong>3. Compare two prompts.</strong> Problem 1 has 103 tokens. Both prompts start with the same 23-token chat template. Their first block is identical, but block 2 already contains the problem text. The hashes differ from there.</p> <p><strong>4. Store the owners.</strong> For every hash, the router remembers which replicas served it and which hashes came after it (we cap the successor set at two because we only need to know whether a block has one continuation or several). After a few prompts, the template block <code>h1</code> is owned by both replicas and already has several successors, <code>h2</code> to <code>h8</code> are owned by A only and each has a single successor, and <code>h2'</code> to <code>h6'</code> are owned by B only.</p> <p>In practice, every prompt in a run starts with the same tokens. Here, it is the chat template and the system prompt, which amount to the first 23 tokens of all 1,460 problems. In an agent setting, it would be the tool descriptions, and in a multi-turn environment it would be the shared conversation history. These blocks are in every replica's cache within seconds, so matching on them tells us nothing about where a particular prompt lives.</p> <p>A block is <em>common</em> if every replica has served it, or if it has more than one successor. Common blocks are ignored during routing because they do not identify a particular prompt. More on this below!</p> <p><strong>5. Pick a replica.</strong> The router counts how many leading blocks match on each replica and removes the common prefix. What is left is the number of blocks specific to this prompt. Then:</p> <ul> <li>If one replica has specific blocks and it is not swamped, meaning it is at most 8 requests ahead of the least-loaded replica, the request goes there. We call this an <strong>affinity</strong> hit.</li> <li>If a replica has specific blocks but it is more than 8 requests ahead, we give up on the cache and send the request to the least-loaded replica. We call this a <strong>spill</strong>.</li> <li>If no replica has specific blocks, this is a new prompt. It goes to the least-loaded replica, round-robin on ties. We call this <strong>unmatched</strong>.</li> </ul> <p>Here is the rule applied to four requests. Start from a state where replicas A and B both have 3 requests in flight, and only the template block <code>h1</code> is known on both replicas.</p> <ul> <li><strong>Request 1, problem 0, rollout 1.</strong> Both replicas match one block, the template, and that block is common. So nothing specific matches anywhere. The request is unmatched, both replicas are equally loaded, and round-robin sends it to A. The router records <code>h2</code> to <code>h8</code> as owned by A. A now has 4 requests in flight.</li> <li><strong>Request 2, problem 0, rollout 2.</strong> Same prompt. A matches all 8 blocks, B matches only the template. After removing the one common block, A has 7 specific blocks and B has none. A is only 1 request ahead of B, well within the limit of 8, so the request goes to A. This is an affinity hit: A already has the whole prompt in its KV cache.</li> <li><strong>Request 3, problem 1, rollout 1.</strong> A new prompt. Both replicas match only the template block, so nothing specific matches. The request is unmatched and goes to the least-loaded replica, B, which has 3 in flight against A's 5. The router records <code>h2'</code> to <code>h6'</code> as owned by B.</li> <li><strong>Request 4, problem 0, rollout 9.</strong> Suppose that by now A has 12 requests in flight while B is back to 3. A still has the 7 specific blocks, but it is now 9 requests ahead of B, more than the limit. The request spills to B. B prefills problem 0 once, and the router records <code>h2</code> to <code>h8</code> as owned by B as well. Every replica has now served those blocks, so problem 0 becomes common too, and its later rollouts are placed by load alone.</li> </ul> <p><strong>6. Reuse the prefill.</strong> Request 2 is the reason for doing all this. It reuses the prefill computed by Request 1: blocks 1 to 8 are already in A's KV cache, so A skips straight to decoding the completion. Had it gone to B, B would have prefilled all 135 tokens again while A's cache sat unused. Request 3 shows why the <code>common</code> rule is needed. Without it, the shared chat template would make every new prompt look like a cache hit. Request 4 keeps the load bounded. Saving one prefill is not worth letting a replica fall far behind.</p> <pre><code class="language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">choose</span>(<span class="hljs-params">self, upstreams, model, prompt</span>): hashes = self.block_hashes(model, prompt) <span class="hljs-comment"># chained blake2b over 16-token blocks, seeded with `model`</span> matched = self.matched_prefix(hashes) <span class="hljs-comment"># per replica: leading blocks it has served</span> common = self.common_prefix_len(hashes) <span class="hljs-comment"># leading blocks that identify no prompt (see below)</span> specific = [<span class="hljs-built_in">max</span>(<span class="hljs-number">0</span>, m - common) <span class="hljs-keyword">for</span> m <span class="hljs-keyword">in</span> matched] <span class="hljs-comment"># what actually distinguishes replicas</span> least = <span class="hljs-built_in">min</span>(u.inflight <span class="hljs-keyword">for</span> u <span class="hljs-keyword">in</span> upstreams) best = <span class="hljs-built_in">max</span>(<span class="hljs-built_in">range</span>(self.n), key=<span class="hljs-keyword">lambda</span> i: (specific[i], -upstreams[i].inflight)) <span class="hljs-keyword">if</span> specific[best] &gt; <span class="hljs-number">0</span> <span class="hljs-keyword">and</span> upstreams[best].inflight - least &lt;= self.cfg.imbalance: pick = best <span class="hljs-comment"># affinity: the replica that has this prompt, and is not swamped</span> <span class="hljs-keyword">else</span>: candidates = [i <span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> <span class="hljs-built_in">range</span>(self.n) <span class="hljs-keyword">if</span> upstreams[i].inflight == least] pick = candidates[self.rr % <span class="hljs-built_in">len</span>(candidates)] <span class="hljs-comment"># spill or new prompt: least-loaded, round robin on ties</span> self.rr += <span class="hljs-number">1</span> ...record `pick` <span class="hljs-keyword">as</span> an owner of every block, <span class="hljs-keyword">and</span> each block<span class="hljs-string">'s successor...</span> <span class="hljs-string"> return upstreams[pick]</span> </code></pre> <p>The <code>common</code> prefix is the annoying part. Every request starts with the same system prompt and chat template. A simple longest-prefix match would give the first replica a match for almost every new prompt. We detect the shared prefix through fan-out instead: a block with several different successors is common, while a block that always leads to the same successor belongs to a particular prompt. Only the blocks after that common prefix count as affinity.</p> <h3 class="relative group flex items-baseline"> <a id="broadcasting-the-adapter" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#broadcasting-the-adapter" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Broadcasting the adapter </span> </h3> <p>The proxy also needs to broadcast the adapter loads to every replica. We treat the operation as <strong>all-or-nothing</strong>. Each replica has its own bucket mount, so they do not necessarily see a new adapter at exactly the same time. A <code>No adapter found for &lt;path&gt;</code> error usually means that one bucket mount has not caught up yet, and we retry only that replica. For any other error, we unload the adapter from the replicas that accepted it so that a policy name never exists on only part of the replicas.</p> <pre><code class="language-python"><span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">load_one</span>(<span class="hljs-params">u</span>): <span class="hljs-keyword">while</span> <span class="hljs-literal">True</span>: status, _, out = <span class="hljs-keyword">await</span> send(u, <span class="hljs-string">"POST"</span>, <span class="hljs-string">"/v1/load_lora_adapter"</span>, headers, body) <span class="hljs-keyword">if</span> status == <span class="hljs-number">200</span> <span class="hljs-keyword">or</span> <span class="hljs-string">"No adapter found"</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> out.decode() <span class="hljs-keyword">or</span> time.monotonic() &gt; deadline: <span class="hljs-keyword">return</span> u, status, out <span class="hljs-keyword">await</span> asyncio.sleep(cfg.lora_retry_s) <span class="hljs-comment"># this replica's mount has not seen the directory yet</span> results = <span class="hljs-keyword">await</span> asyncio.gather(*(load_one(u) <span class="hljs-keyword">for</span> u <span class="hljs-keyword">in</span> ups)) <span class="hljs-keyword">if</span> <span class="hljs-built_in">any</span>(st != <span class="hljs-number">200</span> <span class="hljs-keyword">for</span> _, st, _ <span class="hljs-keyword">in</span> results): <span class="hljs-keyword">await</span> asyncio.gather(*(send(u, <span class="hljs-string">"POST"</span>, <span class="hljs-string">"/v1/unload_lora_adapter"</span>, headers, unload) <span class="hljs-keyword">for</span> u, st, _ <span class="hljs-keyword">in</span> results <span class="hljs-keyword">if</span> st == <span class="hljs-number">200</span>)) <span class="hljs-keyword">return</span> web.Response(status=<span class="hljs-number">504</span> <span class="hljs-keyword">if</span> timed_out <span class="hljs-keyword">else</span> st, text=<span class="hljs-string">"rolled back on the others"</span>) </code></pre> <p>We also broadcast <code>/pause</code>, <code>/resume</code> and <code>/v1/unload_lora_adapter</code> in the same way. <code>/health</code> returns 200 only if every replica is healthy. <code>/server_info</code> and <code>/v1/models</code> only need one answer. From TRL's point of view, the proxy is a single <code>data_parallel_size=1</code> server, so it selects adapter-only sync.</p> <p>We initially wondered whether a Python asyncio proxy would become a bottleneck. It does not (at least at this scale). There are at most 128 non-streaming JSON requests in flight, and routing only computes a few hashes. One thread handles this easily. A more refined router that needs to handle more traffic would probably need to be written in a faster language (We see you 🦀).</p> <h2 class="relative group flex items-baseline"> <a id="full-run-results" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#full-run-results" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Full run results </span> </h2> <p>The numbers below come from the trainer's <a href="https://huggingface.co/docs/trl/en/async_grpo_trainer#logged-metrics">logged metrics</a> on trackio. The run uses <code>Qwen/Qwen2.5-Math-1.5B</code>, LoRA <code>r=1</code> on <code>all-linear</code>, 128 completions per step and 8 rollouts per prompt. It runs for 500 steps and saves a checkpoint every 50 steps. The trainer uses an <code>h200x2</code> Job and each of the two vLLM replicas uses one <code>h200</code> Job. Running all three costs ~$20 per hour.</p> <h3 class="relative group flex items-baseline"> <a id="weight-sync" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#weight-sync" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Weight sync </span> </h3> <div class="max-w-full overflow-auto"> <table> <thead><tr> <th>per sync, trainer's clock, 126 syncs</th> <th>before</th> <th>now (p50)</th> </tr> </thead><tbody><tr> <td>whole sync</td> <td>30.8 s</td> <td><strong>8.5 s</strong> (min 6.6, max 9.2)</td> </tr> <tr> <td>of which: pause both replicas</td> <td>0.3 s</td> <td>0.3 s</td> </tr> <tr> <td>adapter all-gather and save to the bucket</td> <td>0.6 s</td> <td>1.1 s</td> </tr> <tr> <td>both replicas accept the adapter</td> <td>~29 s</td> <td>~7 s</td> </tr> </tbody> </table> </div> <p>All 252 adapter loads succeeded 🎉: 126 syncs times 2 replicas. Six succeeded on the second attempt and 246 on the third.</p> <h3 class="relative group flex items-baseline"> <a id="routing" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#routing" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Routing </span> </h3> <p>At the end of the run, after 64,728 rollouts, the proxy's counters read:</p> <pre><code>routed [31928, 32800] affinity 54712 spilled 820 unmatched 9196 </code></pre> <p>With 8 rollouts per prompt, at least one of the eight requests must be cold. The theoretical minimum is therefore 12.5 %. The router gets 14.2 % unmatched requests, 84.5 % affinity hits, and 1.3 % spills. There is not much left to gain here unless we start looking at each replica's load using deeper inference-side metrics based on real measured load.</p> <h3 class="relative group flex items-baseline"> <a id="where-the-time-goes" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#where-the-time-goes" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Where the time goes </span> </h3> <p>The first configuration has a pretty obvious problem: the trainer is the bottleneck, not generation. Over the 500 steps, we have:</p> <div class="max-w-full overflow-auto"> <table> <thead><tr> <th>per optimizer step, p50</th> <th></th> </tr> </thead><tbody><tr> <td>step</td> <td>22.9 s</td> </tr> <tr> <td>forward + backward</td> <td>21.9 s</td> </tr> <tr> <td>waiting for rollouts</td> <td>0.02 s</td> </tr> <tr> <td>rollout queue occupancy</td> <td>476 of 512</td> </tr> <tr> <td>trainer MFU</td> <td>3.9 %</td> </tr> </tbody> </table> </div> <p>The rollout queue stays full, and the worker is mostly blocked by backpressure. The second replica is basically useless in this configuration. We'll see later in the post how we went through runs that moved the bottleneck between training and generation to make the run 3.9× faster.</p> <h3 class="relative group flex items-baseline"> <a id="reward" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#reward" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Reward </span> </h3> <p><a href="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/asyncgrpo-lora-hfjobs/fig0-r1-dp2-reward.png" rel="nofollow"><img src="https://imgproxy.citrusreader.com/qrfP-14bKkzWwlle5Dh3fdWd4Qv7FMPGAmDF0sjMrgE/raw:1/aHR0cHM6Ly9odWdnaW5nZmFjZS5jby9kYXRhc2V0cy9odWdnaW5nZmFjZS9kb2N1bWVudGF0aW9uLWltYWdlcy9yZXNvbHZlL21haW4vYmxvZy9hc3luY2dycG8tbG9yYS1oZmpvYnMvZmlnMC1yMS1kcDItcmV3YXJkLnBuZw" alt="Figure 1" referrerpolicy="no-referrer"></a></p> <p><em>Figure 1. trackio run <code>r1-dp2</code>. Panels: <code>reward</code> with its 20-step rolling mean and 50-step block means, and <code>ratio</code> on a 0.99 to 1.01 axis. Reward climbs from 0.15 to 0.44 over 500 steps; <code>ratio</code> stays between 0.9993 and 1.0004 throughout.</em></p> <p>500 steps took 3 h 27 min. Mean reward goes from 0.145 over the first 20 steps to 0.438 over the last 20. More importantly for this test, <code>ratio</code> stays at 1.000 for every step! The policy served by vLLM always matches the one used by the trainer to score the rollout. This held across all 126 syncs. Mean staleness was 1.5 policy versions, against a maximum of 4. The <a href="https://huggingface.co/spaces/aminediroHF/async-grpo-lora-buckets">trackio dashboard</a> has the full curves.</p> <p>We have our undeniable proof that LoRA AsyncGRPO works! Let's now see how we can improve our training runs by looking at the recent detailed AsyncGRPO metrics.</p> <h2 class="relative group flex items-baseline"> <a id="chasing-the-bottleneck-ping-pong" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#chasing-the-bottleneck-ping-pong" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Chasing the bottleneck ping-pong </span> </h2> <p>Async RL is a pipeline between training and generation. Making one side faster does nothing if the other side cannot keep up. Fortunately, in <code>AsyncGRPOTrainer</code> we've added enough timings and metrics to see this directly.</p> <p>All the useful metrics are documented in the <a href="https://huggingface.co/docs/trl/en/async_grpo_trainer#logged-metrics">Logged metrics</a> section. <code>perf/rollout_wait_s</code> tells us how long the trainer waits for samples. <code>rollout/backpressure_s</code> tells us how long generation waits for space in the rollout queue. They are diametrically opposed to each other and should not both be high. Together with the queue size, they tell us which side is slow.</p> <p>We ran five experiments. Each one starts from a problem visible in the previous run's dashboard. Unless mentioned otherwise, the model, recipe and three-Job layout stay the same. The names below are the trackio run names.</p> <h3 class="relative group flex items-baseline"> <a id="reading-the-dashboard" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#reading-the-dashboard" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Reading the dashboard </span> </h3> <p>We keep these four groups of metrics visible:</p> <ul> <li><strong><code>perf/step_s</code></strong> and <strong><code>perf/fwd_bwd_s</code></strong>: how long an optimizer step takes, and how much of it is forward+backward. If a step takes nearly as long as the forward+backward, then the trainer is clearly compute-bound.</li> <li><strong><code>perf/rollout_wait_s</code></strong>: how long the trainer sat waiting for samples before it could start a step. Near zero means generation is ahead of training. Samples are available immediately to be trained on.</li> <li><strong><code>sample/rollout_queue_size</code></strong> against <code>queue_maxsize</code>: the buffer between the two sides. Full means generation is being throttled; empty means the trainer is starving.</li> <li><strong><code>rollout/backpressure_s</code></strong> and <strong><code>rollout/score_block_s</code></strong>: how long the rollout worker sat blocked because that buffer was full. The worker is a two-stage pipeline: generation hands finished groups to a scoring stage, and scoring pushes scored samples into the rollout buffer. When the buffer is full, scoring cannot enqueue and blocks, which is <code>rollout/backpressure_s</code>. Scoring then stops draining its own input queue, so generation cannot hand over the next group either, which is <code>rollout/score_block_s</code>. Both are the same stall, seen first at the scoring stage and then propagated back to generation.</li> </ul> <p>The diagnosis is simple: a full queue with zero rollout wait and high backpressure means the trainer is too slow. An empty queue with rising rollout wait and no backpressure means generation is too slow. Comparing <code>perf/mfu_wall_clock</code> with <code>perf/mfu_fwd_bwd</code> also shows how much time the trainer GPUs spend waiting instead of training.</p> <h3 class="relative group flex items-baseline"> <a id="run-1-r1-dp2-a-trainer-that-cannot-keep-up" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#run-1-r1-dp2-a-trainer-that-cannot-keep-up" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Run 1, <code>r1-dp2</code>: a trainer that cannot keep up </span> </h3> <p><a href="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/asyncgrpo-lora-hfjobs/fig1-r1-dp2-trainer-bound.png" rel="nofollow"><img src="https://imgproxy.citrusreader.com/zudD71X_dUULrQnbhkv4oBF5Q-8I_flmdHj4XtXs0nA/raw:1/aHR0cHM6Ly9odWdnaW5nZmFjZS5jby9kYXRhc2V0cy9odWdnaW5nZmFjZS9kb2N1bWVudGF0aW9uLWltYWdlcy9yZXNvbHZlL21haW4vYmxvZy9hc3luY2dycG8tbG9yYS1oZmpvYnMvZmlnMS1yMS1kcDItdHJhaW5lci1ib3VuZC5wbmc" alt="Figure 2" referrerpolicy="no-referrer"></a></p> <p><em>Figure 2. trackio run <code>r1-dp2</code>. Panels: <code>perf/step_s</code>, <code>perf/fwd_bwd_s</code>, <code>sample/rollout_queue_size</code>, <code>rollout/backpressure_s</code>. Step time and forward+backward overlap almost completely; the queue sits pinned near 476 of 512 and backpressure never drops below 11 s per rollout group: trainer-bound.</em></p> <p><code>perf/step_s</code> is 22.9 s and <code>perf/fwd_bwd_s</code> is 21.9 s. Forward and backward take 96 % of the step time. The queue stays full and the trainer waits only 0.02 s for rollouts, and the rollout worker spends 15 seconds per group blocked by backpressure. The two vLLM replicas generate faster than the trainer consumes. The reported 4.6k tokens/s is not their actual limit; they simply have nowhere to put more output.</p> <p>The batch metrics explain the terrible 3.9 % MFU. <code>batch/microbatches_per_step</code> is 64 and <code>batch/samples_per_row</code> is 1.0. Each rank processes one sequence of around 1.2k tokens, 64 times per step. This comes from the reference recipe's <code>per_device_train_batch_size=1</code>. For a 1.5B model on an H200, this is completely latency-bound.</p> <h3 class="relative group flex items-baseline"> <a id="run-2-r1-dp2-tb16k-pack-the-microbatch" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#run-2-r1-dp2-tb16k-pack-the-microbatch" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Run 2, <code>r1-dp2-tb16k</code>: pack the microbatch </span> </h3> <p>The fix is not to change the batch size. We keep 128 completions per optimizer step and only change how they are laid out on the GPU: instead of one sequence per microbatch, we pack many sequences densely into each row. The trainer supports this through <strong>token-budget batching</strong>. With <code>token_budget &gt; 0</code>, it packs several samples into one padding-free row per rank. An optimizer step processes <code>gradient_accumulation_steps</code> rows. We set <code>token_budget=16384</code> and <code>gradient_accumulation_steps=6</code>.</p> <p><a href="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/asyncgrpo-lora-hfjobs/fig2-r1-dp2-vs-tb16k-packing.png" rel="nofollow"><img src="https://imgproxy.citrusreader.com/M_aPUT3QkmiTTfRCCU6kl4dHe7FZESGDqKxkAnmFwRM/raw:1/aHR0cHM6Ly9odWdnaW5nZmFjZS5jby9kYXRhc2V0cy9odWdnaW5nZmFjZS9kb2N1bWVudGF0aW9uLWltYWdlcy9yZXNvbHZlL21haW4vYmxvZy9hc3luY2dycG8tbG9yYS1oZmpvYnMvZmlnMi1yMS1kcDItdnMtdGIxNmstcGFja2luZy5wbmc" alt="Figure 3" referrerpolicy="no-referrer"></a></p> <p><em>Figure 3. trackio runs <code>r1-dp2</code> and <code>r1-dp2-tb16k</code> overlaid over their first 154 steps. Panels: <code>batch/samples_per_row</code>, <code>batch/microbatches_per_step</code>, <code>perf/step_s</code>, <code>perf/fwd_bwd_s</code>, <code>perf/mfu_fwd_bwd</code>, <code>rollout/generated_tok_s</code>. Packing takes samples per row from 1 to 13, microbatches from 64 to 6, step time from 23 s to 5.9 s, and generation from 4.2k to 27.5k tok/s with no change on the vLLM side.</em></p> <p><code>batch/samples_per_row</code> goes from 1.0 to ~12.7 and the number of microbatches drops from 64 to 6. The rows are 95 % full! Forward and backward fall from 21.9 s to 5.6 s, while MFU rises from 3.9 % to 19 %. We now train on around 150 samples per step because the rows pack better than the mean-length estimate predicted.</p> <p>Generation also jumps from 4.6k to 25k tokens/s, even though we changed nothing on the vLLM side. The queue is no longer constantly full, so the replicas can finally run. This is why we do not like optimizing pipeline stages in isolation. We need to be careful to evaluate the whole system, as a slow stage can hide the real performance of everything before it.</p> <h3 class="relative group flex items-baseline"> <a id="run-3-r1-dp2-tb16k-nockpt-stop-recomputing-the-forward" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#run-3-r1-dp2-tb16k-nockpt-stop-recomputing-the-forward" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Run 3, <code>r1-dp2-tb16k-nockpt</code>: stop recomputing the forward </span> </h3> <p><code>perf/fwd_s</code> is 1.34 s while <code>perf/fwd_bwd_s</code> is 5.6 s. A normal backward costs roughly twice the forward, and with frozen base weights it should be closer to once. A ratio of 3.2 is suspicious.</p> <p>The reason is that <code>AsyncGRPOConfig</code> defaults to <code>gradient_checkpointing=True</code>. Every microbatch recomputes its forward during the backward. This also explains why a 16k-token row only uses 25 GB on a 141 GB H200! It's a memory optimization for this trainer, but we don't need it in this specific case: the model is small enough to fit into VRAM with the activations kept for the backward.</p> <p><a href="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/asyncgrpo-lora-hfjobs/fig3-tb16k-vs-nockpt-crossover.png" rel="nofollow"><img src="https://imgproxy.citrusreader.com/L_FeSCY8nlnJ1K8BqKMDbKLVY8klgsWlT9E3OH03LIo/raw:1/aHR0cHM6Ly9odWdnaW5nZmFjZS5jby9kYXRhc2V0cy9odWdnaW5nZmFjZS9kb2N1bWVudGF0aW9uLWltYWdlcy9yZXNvbHZlL21haW4vYmxvZy9hc3luY2dycG8tbG9yYS1oZmpvYnMvZmlnMy10YjE2ay12cy1ub2NrcHQtY3Jvc3NvdmVyLnBuZw" alt="Figure 4" referrerpolicy="no-referrer"></a></p> <p><em>Figure 4. trackio runs <code>r1-dp2-tb16k</code> and <code>r1-dp2-tb16k-nockpt</code> overlaid over their first 134 steps. Panels: <code>perf/fwd_s</code>, <code>perf/fwd_bwd_s</code>, <code>perf/weight_sync_s</code>, <code>sample/rollout_queue_size</code>, <code>perf/rollout_wait_s</code>, <code>perf/mfu_fwd_bwd</code>. Forward+backward drops by one forward; the queue falls from ~420 to ~60, and rollout wait rises from 0.02 s to 0.5 s: the bottleneck shifts to generation.</em></p> <p>With <code>gradient_checkpointing=False</code>, forward and backward drop to 4.6 s, almost exactly one forward less, and MFU reaches 23 %. The queue now falls to 71 and rollout wait rises from 0.04 s to 0.6 s. The trainer consumes samples faster than two replicas generate them. We <em>successfully</em> moved the bottleneck to generation.</p> <p>This exposes two more costs. A 7.6-second weight sync every four steps now takes 25 % of wall-clock time. It was only 8 % when each step took 23 seconds. Also, backward is still 2.5 times slower than forward. With frozen base weights, there are around 2 seconds per step that do not look like normal model math.</p> <h3 class="relative group flex items-baseline"> <a id="run-4-r1-dp3-tb16k-nockpt-three-replicas-and-a-surprise" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#run-4-r1-dp3-tb16k-nockpt-three-replicas-and-a-surprise" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Run 4, <code>r1-dp3-tb16k-nockpt</code>: three replicas, and a surprise </span> </h3> <p>Since generation was now too slow, we added a third replica. We also reduced the adapter retry interval from 2 s to 0.5 s in the proxy and disabled <code>fsdp_reshard_after_forward</code> to check whether FSDP2 re-gathers caused the extra 2 seconds in backward.</p> <p><a href="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/asyncgrpo-lora-hfjobs/fig4-dp2-vs-dp3-inflight-cap.png" rel="nofollow"><img src="https://imgproxy.citrusreader.com/crnUE5Ua1RDFM947v5FmtGBtNliIkVJFdlbUN1W8ScE/raw:1/aHR0cHM6Ly9odWdnaW5nZmFjZS5jby9kYXRhc2V0cy9odWdnaW5nZmFjZS9kb2N1bWVudGF0aW9uLWltYWdlcy9yZXNvbHZlL21haW4vYmxvZy9hc3luY2dycG8tbG9yYS1oZmpvYnMvZmlnNC1kcDItdnMtZHAzLWluZmxpZ2h0LWNhcC5wbmc" alt="Figure 5" referrerpolicy="no-referrer"></a></p> <p><em>Figure 5. trackio runs <code>r1-dp2-tb16k-nockpt</code> and <code>r1-dp3-tb16k-nockpt</code> overlaid over their first 134 steps. Panels: <code>perf/weight_sync_s</code>, <code>rollout/generated_tok_s</code>, <code>rollout/inflight</code>, <code>perf/fwd_bwd_s</code>. Sync falls from 7.6 s to 5.8 s; generation and forward+backward do not move; <code>rollout/inflight</code> reads 128 in both runs, which is the cap the third replica ran into.</em></p> <p>Weight sync falls from 7.6 s to 5.8 s, so the shorter retry helps. Forward and backward stay at 4.6 s, which rules out resharding. Generation moves from 25k to only 26k tokens/s. The third replica does basically nothing.</p> <p>The reason was sitting in <code>rollout/inflight</code>: 128 in every run. The proxy shows those requests split as 44 + 43 + 41 across the three replicas. <code>max_inflight_tasks</code> limits concurrency for the whole rollout worker, not per replica. A 1.5B model on an H200 processes 43 and 130 concurrent sequences at almost the same cost per token. Splitting 128 requests over three GPUs gives nearly the same throughput as splitting them over two.</p> <p>So vLLM was not the limit. Our own client-side constant was. We had set it conservatively because we did not know how hundreds of long HTTPS requests would behave through the public Jobs proxy. At this point, 130,000 rollout completions had crossed it without a single transport error.</p> <h3 class="relative group flex items-baseline"> <a id="run-5-r1-dp3-inflight384-lift-the-cap-on-in-flight-requests" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#run-5-r1-dp3-inflight384-lift-the-cap-on-in-flight-requests" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Run 5, <code>r1-dp3-inflight384</code>: lift the cap on in-flight requests </span> </h3> <p><code>max_inflight_tasks=384</code> and <code>queue_maxsize=768</code>, nothing else.</p> <p><a href="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/asyncgrpo-lora-hfjobs/fig5-all-runs-scoreboard.png" rel="nofollow"><img src="https://imgproxy.citrusreader.com/fVDBdZ-qCJh4O6UJLmv65zaq9l1KIkVJBzWTsIa-Klc/raw:1/aHR0cHM6Ly9odWdnaW5nZmFjZS5jby9kYXRhc2V0cy9odWdnaW5nZmFjZS9kb2N1bWVudGF0aW9uLWltYWdlcy9yZXNvbHZlL21haW4vYmxvZy9hc3luY2dycG8tbG9yYS1oZmpvYnMvZmlnNS1hbGwtcnVucy1zY29yZWJvYXJkLnBuZw" alt="Figure 6" referrerpolicy="no-referrer"></a></p> <p><em>Figure 6. All five trackio runs (<code>r1-dp2</code>, <code>r1-dp2-tb16k</code>, <code>r1-dp2-tb16k-nockpt</code>, <code>r1-dp3-tb16k-nockpt</code>, <code>r1-dp3-inflight384</code>) overlaid, x-axis in steps. Panels: <code>perf/step_s</code>, <code>reward</code>, <code>sample/rollout_queue_size</code>, <code>sample/staleness_mean</code>. Step time falls from 22.9 s to 4.8 s across the series while the reward curves stay on top of each other; the last run's queue refills to ~690 of 768 and its staleness settles at 2. Runs 2 to 4 were stopped early once the dashboard had answered the question.</em></p> <p>With 384 requests in flight, each replica gets 128. The queue quickly fills to around 690 out of 768 and stays there. Backpressure returns to 5 seconds and rollout wait falls to 0.03 seconds. Training is the bottleneck again! Forward and backward take 4.6 seconds, weight sync adds an amortized 1.5 seconds, and median step time is 4.8 seconds.</p> <p>Mean staleness rises from 1.5 to 2.0 versions because samples wait longer in the larger queue. This is still below <code>max_staleness=4</code>, and <code>ratio</code> remains very close to 1.000.</p> <h3 class="relative group flex items-baseline"> <a id="the-scoreboard" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#the-scoreboard" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> The scoreboard </span> </h3> <div class="max-w-full overflow-auto"> <table> <thead><tr> <th>500 steps</th> <th>run 1 <code>r1-dp2</code></th> <th>run 5 <code>r1-dp3-inflight384</code></th> </tr> </thead><tbody><tr> <td>wall clock</td> <td>3 h 27 min</td> <td><strong>53 min</strong></td> </tr> <tr> <td><code>perf/step_s</code>, p50</td> <td>22.9 s</td> <td>4.8 s</td> </tr> <tr> <td><code>perf/fwd_bwd_s</code>, p50</td> <td>21.9 s</td> <td>4.6 s</td> </tr> <tr> <td><code>perf/mfu_fwd_bwd</code></td> <td>3.9 %</td> <td>23.5 %</td> </tr> <tr> <td><code>batch/samples_per_step</code></td> <td>128</td> <td>168</td> </tr> <tr> <td>samples trained</td> <td>64 000</td> <td>84 078</td> </tr> <tr> <td><code>perf/weight_sync_s</code>, p50</td> <td>8.5 s</td> <td>6.2 s</td> </tr> <tr> <td><code>sample/staleness_mean</code></td> <td>1.5</td> <td>2.0</td> </tr> <tr> <td>reward, first 20 → last 20 steps</td> <td>0.145 → 0.438</td> <td>0.145 → 0.416</td> </tr> </tbody> </table> </div> <p><a href="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/asyncgrpo-lora-hfjobs/fig6-reward-vs-wallclock.png" rel="nofollow"><img src="https://imgproxy.citrusreader.com/iOObaiFhH8dPjSvrTNoOGtI8P9TqgVnNbksswmzwzFQ/raw:1/aHR0cHM6Ly9odWdnaW5nZmFjZS5jby9kYXRhc2V0cy9odWdnaW5nZmFjZS9kb2N1bWVudGF0aW9uLWltYWdlcy9yZXNvbHZlL21haW4vYmxvZy9hc3luY2dycG8tbG9yYS1oZmpvYnMvZmlnNi1yZXdhcmQtdnMtd2FsbGNsb2NrLnBuZw" alt="Figure 7" referrerpolicy="no-referrer"></a></p> <p><em>Figure 7. trackio runs <code>r1-dp2</code> and <code>r1-dp3-inflight384</code>, reward against wall-clock minutes since the first optimizer step. Same recipe, same 500 steps, same final reward; run 5 gets there in 52 minutes instead of 3 h 26 min.</em></p> <p>The final run is 3.9× faster and trains on 31 % more samples, with basically the same reward curve. Packing, disabling checkpointing and raising the in-flight limit made the difference. In each case, the dashboard made this clear within the first ten minutes.</p> <h2 class="relative group flex items-baseline"> <a id="try-it" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#try-it" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Try it </span> </h2> <pre><code class="language-sh">git <span class="hljs-built_in">clone</span> https://github.com/AmineDiro/hfjobs-lora-buckets &amp;&amp; <span class="hljs-built_in">cd</span> hfjobs-lora-buckets hf auth login MAX_STEPS=20 RUN_TAG=smoke ./run_all.sh --<span class="hljs-built_in">wait</span> <span class="hljs-comment"># ~15 min, three Jobs, cancels the servers when done</span> MAX_STEPS=500 ./run_all.sh --<span class="hljs-built_in">wait</span> <span class="hljs-comment"># run 1: the reference batch shape, ~3.5 h</span> TOKEN_BUDGET=16384 GRAD_ACCUM=6 GRADIENT_CHECKPOINTING=0 PROXY_LORA_RETRY_S=0.5 \ MAX_INFLIGHT=384 QUEUE_MAXSIZE=768 MAX_STEPS=500 ./run_all.sh --<span class="hljs-built_in">wait</span> <span class="hljs-comment"># run 5: same recipe, ~55 min</span> </code></pre> <h2 class="relative group flex items-baseline"> <a id="references" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/asyncgrpo-lora-hfjobs#references" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> References </span> </h2> <ul> <li>John Schulman et al., <a href="https://thinkingmachines.ai/blog/lora/" rel="nofollow">LoRA Without Regret</a>, Thinking Machines Lab, September 2025. The case that rank-1 LoRA matches full fine-tuning for policy-gradient RL, and why.</li> <li>TRL, <a href="https://huggingface.co/docs/trl/en/async_grpo_trainer"><code>AsyncGRPOTrainer</code></a> and its <a href="https://huggingface.co/docs/trl/en/async_grpo_trainer#logged-metrics">logged metrics</a>.</li> <li>TRL <a href="https://github.com/huggingface/trl/pull/7017" rel="nofollow">PR #7017</a>: PEFT/LoRA support for <code>AsyncGRPOTrainer</code> with adapter-only vLLM sync, released in TRL v1.14.</li> <li>Hugging Face <a href="https://huggingface.co/docs/huggingface_hub/guides/jobs">Jobs</a> and <a href="https://huggingface.co/docs/hub/storage-buckets">Storage Buckets</a>; <a href="https://github.com/huggingface/hf-mount" rel="nofollow"><code>hf-mount</code></a>.</li> <li><a href="https://github.com/AmineDiro/hf-mount-repro" rel="nofollow"><code>hf-mount-repro</code></a>: the two-script reproduction of the 30-second negative-cache stall.</li> <li>The <a href="https://huggingface.co/spaces/aminediroHF/async-grpo-lora-buckets">trackio dashboard</a> for every run in this post.</li> <li>Penghui Qi, Zichen Liu, Xiangxin Zhou, Tianyu Pang, Chao Du, Wee Sun Lee, Min Lin, <a href="https://huggingface.co/papers/2510.26788">Defeating the Training-Inference Mismatch via FP16</a>, arXiv:2510.26788, 2025. Source of the Sanity dataset <a href="https://huggingface.co/datasets/sail/Sanity-Test-R1D-1.5B"><code>sail/Sanity-Test-R1D-1.5B</code></a> and of the LoRA recipe, <a href="https://github.com/sail-sg/Precision-RL" rel="nofollow"><code>sail-sg/Precision-RL</code></a>, <code>oat/scripts/lora/bf16_grpo_tis_lora.sh</code>.</li> </ul> <pre><code class="language-bibtex">@article{qi2025precisionrl, title={Defeating the Training-Inference Mismatch via FP16}, author={Qi, Penghui and Liu, Zichen and Zhou, Xiangxin and Pang, Tianyu and Du, Chao and Lee, Wee Sun and Lin, Min}, journal={arXiv preprint arXiv:2510.26788}, year={2025} } </code></pre> </div>

2026/9/10
阅读更多

Rebuilding AUTOMATIC1111 with Gradio Workflow

<div class="SVELTE_HYDRATER contents" data-target="RepoCodeCopy" data-props="{}"><div></div><!----></div> <div> <!--[0--><a target="_blank" class="btn mb-5 font-sans text-sm no-underline" href="https://github.com/huggingface/blog/blob/main/gradio-workflow-1111.md">Update on GitHub</a><!--]--></div><!----> <!----> <!--[-1--><!--]--> <div class="relative overflow-clip [overflow-clip-margin:3rem] [&amp;>:last-child]:mb-0"><!----> <p><!--[--><!--[0--></p><!--]--><!--]-->In our <a href="https://huggingface.co/blog/gradio-workflow-guide">last post</a>, we built five small <code>gr.Workflow</code> graphs and hinted at what it would take to build something as complex as AUTOMATIC1111's <a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui" rel="nofollow">stable-diffusion-webui</a>. In this post we walk you through <strong>Workflow1111</strong>, where we have rebuilt most of AUTOMATIC1111's feature set as a single workflow canvas.<p></p> <p>Workflow1111 is a graph of <strong>eleven media pipelines</strong> built using <strong>seventy-three nodes</strong>. It brings together SOTA models for text-to-image, hi-resolution fix, image-to-image, prompt-matrix grids, VLM interrogate, detection-to-inpaint masks, ControlNet-style annotators, background removal, PNG Info storing, and image-to-video.</p> <p>You can run any of these pipelines by signing in with your Hugging Face account or providing an access token. Once you sign in, the model calls use your own quota.</p> <p>👉 <strong><a href="https://huggingface.co/spaces/ysharma/Workflow1111">Try Workflow1111</a></strong>, or duplicate the Space and start rewiring it for your own use case.</p> <p>Let's walk the canvas.</p> <h2 class="relative group flex items-baseline"> <a id="whats-on-the-canvas" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#whats-on-the-canvas" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> What's on the canvas </span> </h2> <p>All the media pipelines are built from the same four operator kinds covered in our last post and the <a href="https://gradio.app/guides/workflows#operator-kinds" rel="nofollow">official guide</a>. Each node on the canvas wraps one operator, and the operator's inputs and outputs become the ports you connect edges to. As a quick reference on our four operator kinds: <code>fn</code> is a Python function, <code>model</code> is a model called through <code>InferenceClient</code>, <code>space</code> is another Gradio Space, and <code>dataset</code> is a row from a Hub dataset.</p> <p>Let's go through the pipelines one by one.</p> <h3 class="relative group flex items-baseline"> <a id="text-to-image" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#text-to-image" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Text-to-image </span> </h3> <p><video controls="" autoplay="" loop="" muted="" playsinline="" src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/gradio-workflow1111/txt2img.mp4"></video></p> <p>This is the core pipeline. It has the controls you'd expect from A1111's txt2img tab: negative prompt, steps, CFG, seed, width and height, plus a <code>model_id</code> field for choosing the checkpoint. The prompt goes through a prompt-builder <code>fn</code> node first, which appends the selected style preset and cleans up the text, then into a <code>model</code> node that calls the checkpoint through Inference Providers. A post-process <code>fn</code> node writes the generation parameters into the PNG's metadata on the way out, which is what the PNG Info pipeline reads back later.</p> <h3 class="relative group flex items-baseline"> <a id="hi-resolution-fix" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#hi-resolution-fix" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Hi-resolution fix </span> </h3> <p><video controls="" autoplay="" loop="" muted="" playsinline="" src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/gradio-workflow1111/hires-fix.mp4"></video></p> <p>In Automatic1111, hi-resolution fix first upscales the txt2img output and then runs a second denoising pass. Here it's a two-node detour instead. The text-to-image result goes into a <a href="https://huggingface.co/black-forest-labs/FLUX.1-Kontext-dev">FLUX.1-Kontext</a> <code>model</code> node with a refine instruction ("enhance fine detail and micro-texture, keep the composition identical") and comes back sharper and larger.</p> <h3 class="relative group flex items-baseline"> <a id="image-to-image" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#image-to-image" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Image-to-image </span> </h3> <p><video controls="" autoplay="" loop="" muted="" playsinline="" src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/gradio-workflow1111/img2img.mp4"></video></p> <p>That same Kontext node doubles as the image-to-image tab. Upload an image, describe the change you want, and it returns the edited image.</p> <h3 class="relative group flex items-baseline"> <a id="let-an-llm-write-the-prompt" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#let-an-llm-write-the-prompt" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Let an LLM write the prompt </span> </h3> <p><video controls="" autoplay="" loop="" muted="" playsinline="" src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/gradio-workflow1111/prompt-magic.mp4"></video></p> <p>Start with a rough prompt like "A lighthouse in a storm." This pipeline sends it to a <a href="https://huggingface.co/Qwen/Qwen3-4B-Instruct-2507">Qwen3-4B</a> <code>model</code> node, and a small <code>fn</code> node turns the reply into a clean list of tags, capped at forty: "stormy sea, wet rocks, dramatic composition, low angle shot, volumetric lighting, ominous tone." You can connect any diffusion model node to this output to render the image.</p> <p>There's no custom node involved, unlike in ComfyUI. In a Gradio workflow the LLM and the diffusion model are both ordinary <code>model</code> operators on the same canvas.</p> <h3 class="relative group flex items-baseline"> <a id="read-an-image-back-into-a-prompt" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#read-an-image-back-into-a-prompt" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Read an image back into a prompt </span> </h3> <p><video controls="" autoplay="" loop="" muted="" playsinline="" src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/gradio-workflow1111/interrogate.mp4"></video></p> <p>This is like AUTOMATIC1111's Interrogate button, with a VLM doing the interrogating instead of CLIP. <a href="https://huggingface.co/Qwen/Qwen2.5-VL-7B-Instruct">Qwen2.5-VL</a> looks at a night-market photo and writes a prompt that could have produced it. A <a href="https://huggingface.co/google/vit-base-patch16-224">ViT</a> classifier node reads the same image and returns labels: restaurant 51.9%, tobacco shop 15.6%, toyshop 9.1%.</p> <p>Both nodes use the same image input, so <code>gr.Workflow</code> runs them in parallel and you get both answers in roughly the time it takes to run one.</p> <h3 class="relative group flex items-baseline"> <a id="detection-to-inpaint-mask" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#detection-to-inpaint-mask" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Detection to inpaint mask </span> </h3> <p><video controls="" autoplay="" loop="" muted="" playsinline="" src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/gradio-workflow1111/detect-and-mask.mp4"></video></p> <p>AUTOMATIC1111 makes you paint an inpaint mask by hand. This pipeline generates one from a detector instead. <a href="https://huggingface.co/facebook/detr-resnet-50">DETR</a> finds six objects in a street photo (three people, a dog, a bicycle, and a car), and from there the workflow splits into two branches: one draws the detected boxes on the original image, the other turns them into a mask you can feed into an inpaint pipeline downstream.</p> <p>The drawing and the mask creation both happen locally with Pillow and NumPy. Only the detection call leaves the machine.</p> <h3 class="relative group flex items-baseline"> <a id="prompt-matrix" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#prompt-matrix" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Prompt matrix </span> </h3> <p><video controls="" autoplay="" loop="" muted="" playsinline="" src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/gradio-workflow1111/prompt-matrix.mp4"></video></p> <p>This is like AUTOMATIC1111's prompt matrix. A base prompt, "a lone oak tree," gets combined with four suffixes (at sunrise, in a thunderstorm, under the Milky Way, in autumn fog) by a <code>fn</code> node, and each variant goes to its own text-to-image node. A final node stitches the four results into one contact sheet.</p> <p><code>gr.Workflow</code> has no loop operator, so the four text-to-image nodes sit side by side on the canvas. Since they're at the same dependency depth they run in parallel, and all four images start generating at once.</p> <h3 class="relative group flex items-baseline"> <a id="upscale-and-background-removal" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#upscale-and-background-removal" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Upscale and background removal </span> </h3> <p><video controls="" autoplay="" loop="" muted="" playsinline="" src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/gradio-workflow1111/extras-upscale.mp4"></video></p> <p>This is like the Extras tab in Automatic1111. There are two upscaler nodes, and they take different routes. The first is a local <a href="https://en.wikipedia.org/wiki/Lanczos_resampling" rel="nofollow">Lanczos</a> resample in an <code>fn</code> node, which needs no network call and finishes as fast as Pillow can resize. The second is <a href="https://huggingface.co/spaces/gokaygokay/AuraSR-v2">AuraSR ×4</a>, and it's the first <code>space</code> node on the canvas: it calls a <a href="https://huggingface.co/spaces/gokaygokay/AuraSR-v2">Space</a> on the Hub and treats the result like any other node output.</p> <p><video controls="" autoplay="" loop="" muted="" playsinline="" src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/gradio-workflow1111/extras-background.mp4"></video></p> <p>Background removal works the same way. <a href="https://huggingface.co/spaces/briaai/BRIA-RMBG-2.0">BRIA RMBG-2.0</a> is another <code>space</code> node, so the whole model lives in its own Space and this canvas just calls it in.</p> <h3 class="relative group flex items-baseline"> <a id="annotators" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#annotators" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Annotators </span> </h3> <p><video controls="" autoplay="" loop="" muted="" playsinline="" src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/gradio-workflow1111/annotators.mp4"></video></p> <p>Canny, line art, sketch, luma-depth, and posterize are the preprocessors you'd normally get from the ControlNet extension in Automatic1111. Here, each one is a <code>fn</code> node written in plain NumPy, with no model behind it. On a pre-loaded example photo of a building facade, each annotator takes about half a second on CPU.</p> <p>There are 36 operator nodes in the app, 32 are <code>fn</code> nodes, and 22 of those run entirely in-process without a network call. Roughly two-thirds of the canvas keeps working if you lose your connection. Since these are regular Python functions, you can also test them directly, with no canvas, server, or GPU involved.</p> <h3 class="relative group flex items-baseline"> <a id="png-info" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#png-info" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> PNG Info </span> </h3> <p><video controls="" autoplay="" loop="" muted="" playsinline="" src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/gradio-workflow1111/png-info.mp4"></video></p> <p>AUTOMATIC1111 stores generation details in the PNG's <code>parameters</code> text chunk, and the PNG Info tab reads them back. Workflow1111 does the same. The post-process node on the text-to-image pipeline writes the metadata, and this pipeline reads it back out, including the prompt, negative prompt, steps, CFG, seed, image size, and model.</p> <h3 class="relative group flex items-baseline"> <a id="image-to-video" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#image-to-video" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Image-to-video </span> </h3> <p><video controls="" autoplay="" loop="" muted="" playsinline="" src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/gradio-workflow1111/img2video.mp4"></video></p> <p>The image node that PNG Info reads from also feeds a <a href="https://huggingface.co/Wan-AI/Wan2.2-I2V-A14B">Wan 2.2 I2V A14B</a> node, which animates it; in the demo example a sleeping fox wakes up and starts moving. There's no second upload box because one reference node can feed as many downstream pipelines as you need, so a single upload gets its metadata read and gets animated on the same canvas.</p> <h2 class="relative group flex items-baseline"> <a id="running-models-on-your-own-gpu" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#running-models-on-your-own-gpu" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Running models on your own GPU </span> </h2> <p><video controls="" autoplay="" loop="" muted="" playsinline="" src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/blog/gradio-workflow1111/img2video-zerogpu.mp4"></video></p> <p>So far every model call has gone to someone else's hardware, through Inference Providers or a Space. That's why you can build and run something like Workflow1111 without a GPU of your own.</p> <p>A <code>fn</code> node is just Python, though, so it can equally load a model locally and run it on your own GPU. <a href="https://huggingface.co/spaces/FastVideo/fastvideo-fasth3-preview">FastVideo/fastvideo-fasth3-preview</a> is a <code>gr.Workflow</code> app that does exactly that. It runs <a href="https://huggingface.co/FastVideo/FastVideo-FastH3-4-step-Preview-v1-VSA-DataFree">FastH3</a>, a four-step distillation of <a href="https://huggingface.co/MiniMaxAI/MiniMax-H3">MiniMax-H3</a>, and generates video with a soundtrack on ZeroGPU.</p> <p>The whole app comes down to one bound function:</p> <pre><code class="language-python"><span class="hljs-meta">@spaces.GPU(<span class="hljs-params">duration=get_duration, size=GPU_SIZE</span>)</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">_generate</span>(<span class="hljs-params">prompt_embeds, text_token_tags, height, width, num_frames, seed</span>): ... gr.Workflow(bind={<span class="hljs-string">"generate"</span>: generate, <span class="hljs-string">"status"</span>: status}).launch() </code></pre> <p><a href="https://huggingface.co/docs/hub/spaces-zerogpu">ZeroGPU</a> gives the function a GPU when it needs one, then releases it when the call is done. <code>gr.Workflow</code> doesn't need to know about any of that. It just calls the <code>fn</code> node.</p> <p>This isn't specific to Spaces either. Point <code>bind=</code> to a function that loads a local checkpoint, run <code>.launch()</code> on your own machine, and the Workflow1111 canvas can drive your own GPU.</p> <h2 class="relative group flex items-baseline"> <a id="every-output-is-an-api" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#every-output-is-an-api" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Every output is an API </span> </h2> <p>Every output node on the canvas becomes a REST endpoint, with no routes written by hand. Workflow1111 exposes nine of them: <code>/image</code>, <code>/edited_image</code>, <code>/generated_prompt</code>, <code>/recovered_prompt</code>, <code>/detected_objects</code>, <code>/x_y_grid</code>, <code>/upscaled_local</code>, <code>/annotator_map</code>, and <code>/png_info</code>.</p> <pre><code class="language-python"><span class="hljs-keyword">from</span> gradio_client <span class="hljs-keyword">import</span> Client client = Client(<span class="hljs-string">"ysharma/Workflow1111"</span>, oauth_token=<span class="hljs-string">"hf_..."</span>) image, params, hires = client.predict( <span class="hljs-string">"a red fox in a snowy pine forest"</span>, <span class="hljs-comment"># Prompt</span> <span class="hljs-string">""</span>, <span class="hljs-comment"># Negative prompt</span> <span class="hljs-string">"Cinematic"</span>, <span class="hljs-comment"># Style preset</span> <span class="hljs-string">"enhance fine detail"</span>, <span class="hljs-comment"># Hires refine instruction</span> api_name=<span class="hljs-string">"/image"</span>, ) </code></pre> <p>The same endpoints are also <a href="https://modelcontextprotocol.io/" rel="nofollow">MCP</a> tools. Launch with <code>mcp_server=True</code> (<a href="https://www.gradio.app/guides/building-mcp-server-with-gradio" rel="nofollow">guide</a>) and every output node shows up as a tool an AI assistant can call. Point Claude Code, Cursor, or any MCP client at the server URL:</p> <pre><code class="language-json"><span class="hljs-punctuation">{</span> <span class="hljs-attr">"mcpServers"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">"workflow1111"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">"url"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"https://ysharma-workflow1111.hf.space/gradio_api/mcp/"</span><span class="hljs-punctuation">,</span> <span class="hljs-attr">"headers"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span> <span class="hljs-attr">"X-HF-Token"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"hf_..."</span> <span class="hljs-punctuation">}</span> <span class="hljs-punctuation">}</span> <span class="hljs-punctuation">}</span> <span class="hljs-punctuation">}</span> </code></pre> <p>Now an agent can generate an image, read a prompt back out, or run detection as steps in a larger task, with no glue code. Each caller sends their own token in the <code>X-HF-Token</code> header, so the Space holds none of its own.</p> <h2 class="relative group flex items-baseline"> <a id="where-this-sits-next-to-comfyui" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#where-this-sits-next-to-comfyui" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Where this sits next to ComfyUI </span> </h2> <p>AUTOMATIC1111 gave us the feature list, but the tool Gradio Workflow really gets compared to is ComfyUI, since both are node graphs. For a lot of what people want to build and ship, <code>gr.Workflow</code> covers the same ground.</p> <ul> <li><strong>A node can be hardware you don't own.</strong> It can run through <a href="https://huggingface.co/docs/inference-providers/index">Inference Providers</a>, call any Space on the Hub or any API, or pull from a dataset. That's how Workflow1111 runs without a GPU of its own.</li> <li><strong>Every output becomes a typed REST endpoint.</strong> The endpoints are generated from the graph.</li> <li><strong>Visitors can run workflows under their own identity.</strong> Turn on <a href="https://huggingface.co/docs/hub/spaces-oauth">OAuth</a>, share the public URL, and anyone can sign in and use the app without installing anything.</li> <li><strong>Mix models and modalities on the same canvas.</strong> Diffusion models, LLMs, VLMs, detectors, and video models can all be part of the same workflow.</li> <li><strong>Need something custom? Write a function.</strong> A custom node is a Python function, so it can do whatever Python can.</li> </ul> <p>The result is a multi-model pipeline that people can open in a browser, sign into, use right away, and call from code.</p> <h2 class="relative group flex items-baseline"> <a id="build-your-own" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/gradio-workflow-1111#build-your-own" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Build your own </span> </h2> <p>Workflow1111 has 73 nodes, but it started with just this:</p> <pre><code class="language-python"><span class="hljs-keyword">import</span> gradio <span class="hljs-keyword">as</span> gr <span class="hljs-keyword">def</span> <span class="hljs-title function_">your_function</span>(<span class="hljs-params">text: <span class="hljs-built_in">str</span></span>) -&gt; <span class="hljs-built_in">str</span>: <span class="hljs-keyword">pass</span> gr.Workflow(bind=[your_function]).launch() </code></pre> <p><code>bind=</code> turns your functions into nodes, <code>edges=</code> connects them, and <code>.launch()</code> opens the canvas in your browser so you can keep editing there. When it's ready, <code>gradio deploy</code> puts the whole thing on a Space. The <a href="https://gradio.app/guides/workflows" rel="nofollow">gr.Workflow guide</a> has the full details, including the JSON schema and every operator type.</p> <p>If you'd rather start from something that already works, open <a href="https://huggingface.co/spaces/ysharma/Workflow1111">Workflow1111</a>, hit <strong>Duplicate</strong>, and pick one of the eleven pipelines to change: delete nodes, swap models, rewire the flow. If you'd rather start smaller, the <a href="https://huggingface.co/blog/gradio-workflow-guide">previous post</a> has five workflows you can get running in about a minute each.</p> <p>Whatever you build, post it on X and tag <a href="https://x.com/Gradio" rel="nofollow">@gradio</a>. We'd be happy to amplify your workflows.</p> <!----></div>

2026/9/10
阅读更多

NeoMME: an efficient Multimodal-native and Multilingual Encoder

<div class="SVELTE_HYDRATER contents" data-target="RepoCodeCopy" data-props="{}"><div></div></div> <div> </div> <div class="relative overflow-clip [overflow-clip-margin:3rem] [&amp;>:last-child]:mb-0"> <p></p><a href="https://hf.co/collections/Hcompany/neomme" rel="nofollow"><img src="https://imgproxy.citrusreader.com/uzPVly4IwZd9uGOO1NVeJvwLI-kn5GX2deSi2QWNdyY/raw:1/aHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS9Db2xsZWN0aW9uLUZGRDIxRT9zdHlsZT1mb3ItdGhlLWJhZGdlJmxvZ289aHVnZ2luZ2ZhY2UmbG9nb0NvbG9yPTAwMA" alt="Hugging Face" referrerpolicy="no-referrer"></a> <a href="https://huggingface.co/papers/2609.01657"><img src="https://imgproxy.citrusreader.com/pcFUYnzZKSYBXbHuw3qjhtX1NnFJv8GkzE4bnop7j2A/raw:1/aHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS9IRl9QYXBlci1GRkQyMUU_c3R5bGU9Zm9yLXRoZS1iYWRnZSZsb2dvPWh1Z2dpbmdmYWNlJmxvZ29Db2xvcj0wMDA" alt="Hugging Face Paper" referrerpolicy="no-referrer"></a> <a href="https://arxiv.org/abs/2609.01657" rel="nofollow"><img src="https://imgproxy.citrusreader.com/bAFGxQuEd0yPmvZJnGROe_TMoHQWloIbr7H0xksbbb0/raw:1/aHR0cHM6Ly9pbWcuc2hpZWxkcy5pby9iYWRnZS9hclhpdi0yNjA5LjAxNjU3LWIzMWIxYi5zdmc_c3R5bGU9Zm9yLXRoZS1iYWRnZQ" alt="arXiv" referrerpolicy="no-referrer"></a><p></p> <p align="left"> <img src="https://imgproxy.citrusreader.com/8S5F2acyi5yL6jONRyk_EgmT_nIMUqMJgHiFUn7hoPw/raw:1/aHR0cHM6Ly9naXRodWIuY29tL3Rvbnl3dTcxL2NvbHBhbGktY29va2Jvb2tzL2Jsb2IvNmVmMTMzMmRhNmJjYjQ4YzdlZjFmMTliMjViZmE1NTViZTcwMzFhOC9hc3NldHMvbmVvbW1lL25lb21tZV9sb2dvLndlYnA_cmF3PXRydWU" alt="NeoMME logo" style="max-height: 140px;" referrerpolicy="no-referrer"> </p> <h2 class="relative group flex items-baseline"> <a id="tldr" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#tldr" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> TL;DR </span> </h2> <p>We introduce <em><strong>NeoMME</strong></em>, a family of 260M and 800M multilingual multimodal encoders. Unlike many generative visual language models, <em>NeoMME</em> does not use a separate pretrained vision tower or a causal language model. A single bidirectional Transformer processes both text tokens and raw image patches, and we train the entire model from scratch with a masked discrete-diffusion objective.</p> <p>We fine-tuned <em>NeoMME</em> for visual document retrieval using ColPali's page-image approach. <em>NeoMME</em>-Retriever returns dense and late-interaction embeddings in one forward pass. Both model sizes lie on the ViDoRe v3 Pareto frontier for nDCG@10 and model size. At a matched 2048×2048 image input size on an NVIDIA L40S GPU, the 260M model encodes about 51 pages per second, or about twice ColModernVBERT's throughput. Hierarchical token pooling and asymmetric quantization reduce late-interaction index storage from roughly 1.5 MB to 6 kB per page (255× smaller) while retaining more than 95% of baseline nDCG@10.</p> <p><em>NeoMME</em> is available in Hugging Face Transformers. We release all model checkpoints under the Apache 2.0 license.</p> <ul> <li>🤗 <a href="https://hf.co/collections/Hcompany/neomme" rel="nofollow"><em>NeoMME</em> collection</a></li> <li>📄 <a href="https://huggingface.co/papers/2609.01657">Technical report</a></li> <li>🔎 <a href="https://huggingface.co/spaces/tonywu71/neomme-retriever-demo">Visual RAG demo</a></li> </ul> <h2 class="relative group flex items-baseline"> <a id="why-another-multimodal-encoder" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#why-another-multimodal-encoder" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Why another multimodal encoder? </span> </h2> <p>Many recent visual document retrievers are adapted from pretrained generative visual language models. A separately pretrained vision encoder produces visual features, which a projector maps into the language model's input space. A causal decoder then processes the combined image and text representations. Retrieval, classification, and token labeling do not generate text autoregressively, so they do not require a causal decoder or the parameter and compute overhead of this architecture.</p> <p><a href="https://huggingface.co/blog/modernbert">ModernBERT</a> brought efficient architecture and training improvements to bidirectional encoders. For visual document retrieval, <a href="https://huggingface.co/blog/paultltc/modernvbert">ModernVBERT</a> applied a bidirectional ModernBERT-style text encoder while retaining a separate pretrained SigLIP2 vision tower. We wanted to push this even further by designing and training a multimodal encoder without having to carry over the parameter and compute overhead of a VLM.</p> <p><em><strong>NeoMME</strong></em> (pronounced "nee-oh-me", IPA /ˈniː.oʊ.mi/) is a multilingual, multimodal foundation encoder that generates vector representations for input text and/or images using a single Transformer encoder. It is not based on an existing pretrained vision tower, text encoder, or text decoder.</p> <figure> <img src="https://imgproxy.citrusreader.com/3pWpyqx3WEzpeEFt1nYYRoDhw-TEMft4QQLs1EmA-mI/raw:1/aHR0cHM6Ly9naXRodWIuY29tL3Rvbnl3dTcxL2NvbHBhbGktY29va2Jvb2tzL2Jsb2IvNDgxYzZlYzg4NjZmNmY1ZDM5ZDJkOWRkMDZiNTU0N2MyZDlhNjdmYS9hc3NldHMvbmVvbW1lL2FyY2hpdGVjdHVyZS53ZWJwP3Jhdz10cnVl" alt="Comparison of dual-tower, VLM, ModernVBERT, and NeoMME input paths" referrerpolicy="no-referrer"> <figcaption>Unlike dual-tower and VLM encoders, <em>NeoMME</em> processes image patches and text tokens in one bidirectional Transformer, without a pretrained vision tower or a pretrained text encoder or decoder.</figcaption> </figure> <p>Images and text use the same computational path, so <em>NeoMME</em> can more easily support pretraining, fine-tuning, parallelization, and serving across both modalities.</p> <h2 class="relative group flex items-baseline"> <a id="neomme-encoder-backbone" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#neomme-encoder-backbone" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> <em>NeoMME</em> encoder backbone </span> </h2> <h3 class="relative group flex items-baseline"> <a id="one-transformer-for-images-and-text" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#one-transformer-for-images-and-text" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> One Transformer for images and text </span> </h3> <p><em>NeoMME</em> comes in two sizes, <a href="https://huggingface.co/Hcompany/NeoMME-260M">260M</a> and <a href="https://huggingface.co/Hcompany/NeoMME-800M">800M</a>. Both variants share the same architecture:</p> <ul> <li><strong>Native multimodal inputs:</strong> text inputs use factorized token embeddings, while images are divided into a grid of non-overlapping 32×32 patches and projected with a small MLP. Both enter the same Transformer encoder.</li> <li><strong>Dynamic image resolution:</strong> images keep their aspect ratio and size. This allows the model to use more tokens on a high-resolution, information-dense document page than on a smaller image with less content.</li> <li><strong>Long bidirectional context:</strong> both models have a context length of 16,384 tokens (enough for up to two standard 3840×2160 4K UHD images). Most layers use symmetric sliding-window attention, while every sixth layer and the final layer use global attention.</li> <li><strong>A modern encoder stack:</strong> <em>NeoMME</em> uses recent encoder improvements such as grouped-query attention, query-key normalization, gated attention, 2D rotary position embeddings, and squared-ReLU MLPs, among others.</li> <li><strong>Multilingual text:</strong> we trained a BPE tokenizer with a 131k-token vocabulary from scratch on multilingual text, code, mathematics, and machine-produced image transcripts.</li> </ul> <figure> <img src="https://imgproxy.citrusreader.com/oRWpG5zZndXcpNG91KaRUqOc9-VrToisUeyDX5Dt24Q/raw:1/aHR0cHM6Ly9naXRodWIuY29tL3Rvbnl3dTcxL2NvbHBhbGktY29va2Jvb2tzL2Jsb2IvNDgxYzZlYzg4NjZmNmY1ZDM5ZDJkOWRkMDZiNTU0N2MyZDlhNjdmYS9hc3NldHMvbmVvbW1lL2VuY29kZXItc3RhY2sud2VicD9yYXc9dHJ1ZQ" alt="Alternating sliding-window and global-attention layers in NeoMME" referrerpolicy="no-referrer"> <figcaption>Alternating sliding-window and global-attention layers in the <em>NeoMME</em> encoder stack.</figcaption> </figure> <h3 class="relative group flex items-baseline"> <a id="learning-from-images-through-masked-text" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#learning-from-images-through-masked-text" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Learning from images through masked text </span> </h3> <p>We pretrain <em>NeoMME</em> from scratch as a discrete masked-diffusion text denoiser. For each text-only example, we sample a corruption rate uniformly between 0 and 1. Each eligible text token is then independently masked at that rate.</p> <p>Multimodal examples use corruption rates between 0.3 and 1. The image patches remain visible while <em>NeoMME</em> reconstructs masked text. With light masking, the model can often recover a missing word from the surrounding text alone. For example, "cat" is a plausible completion of "The [MASK] sat on the mat," even without an image. But high masking forces the model to learn image-grounded descriptions with little to no signal from the non-masked input text tokens.</p> <figure> <img src="https://imgproxy.citrusreader.com/k2tqNOb_z4NHGDgBaDv2jyEj_l7ZjrXQvKyir-CL5l0/raw:1/aHR0cHM6Ly9naXRodWIuY29tL3Rvbnl3dTcxL2NvbHBhbGktY29va2Jvb2tzL2Jsb2IvNDgxYzZlYzg4NjZmNmY1ZDM5ZDJkOWRkMDZiNTU0N2MyZDlhNjdmYS9hc3NldHMvbmVvbW1lL21hc2tlZC1kaWZmdXNpb24ud2VicD9yYXc9dHJ1ZQ" alt="Effect of text corruption on the textual and visual evidence available to NeoMME" referrerpolicy="no-referrer"> <figcaption>Higher text corruption removes language-only shortcuts and encourages <em>NeoMME</em> to use visible image evidence.</figcaption> </figure> <p>Pretraining mixes multilingual text, code, mathematics, natural images, and document images. Each model processes about 524 billion packed input tokens, including 290 billion tokens from text-only examples. This text budget is relatively small compared with ModernBERT's 2 trillion training token budget. Hence, we chose the NorMuon optimizer to improve data efficiency during training.</p> <h2 class="relative group flex items-baseline"> <a id="neomme-retriever" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#neomme-retriever" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> <em>NeoMME</em>-Retriever </span> </h2> <p>To get a meaningful downstream evaluation of the backbone, we fine-tune <em>NeoMME</em> for visual document retrieval using the page-image methodology introduced by <a href="https://huggingface.co/blog/manu/colpali">ColPali</a>. While traditional text-based retrieval consists of retrieving text chunks, <em>NeoMME</em>-Retriever ranks document page screenshots and bypasses all the preprocessing OCR steps necessary to extract text from PDFs. Treating the pages as images preserves layout, charts, tables, font type and size, and other visual clues that cannot be captured even by a perfect OCR model.</p> <h3 class="relative group flex items-baseline"> <a id="a-dual-head-design-for-dense-and-late-interaction-retrieval" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#a-dual-head-design-for-dense-and-late-interaction-retrieval" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> A dual-head design for dense and late-interaction retrieval </span> </h3> <p><em>NeoMME</em>-Retriever reuses the <em>NeoMME</em> backbone but adds two jointly trained heads on top of it for retrieval:</p> <ul> <li>The dense head averages the backbone's hidden state vectors into a normalized vector (mean pooling). Dense embeddings are most common today: they are compact and work naturally with approximate nearest-neighbor (ANN) techniques for fast retrieval.</li> <li>The late-interaction head projects each text token or image patch from the backbone's output hidden states to a 128-dimensional normalized vector. Compared to dense embeddings, the finer granularity preserves local matches between individual query tokens and image regions.</li> </ul> <figure> <img src="https://imgproxy.citrusreader.com/G4x9gZtwMfkYRk3Ho-wOLyu423M1k2jyvaAQ1s8uHtY/raw:1/aHR0cHM6Ly9naXRodWIuY29tL3Rvbnl3dTcxL2NvbHBhbGktY29va2Jvb2tzL2Jsb2IvNDgxYzZlYzg4NjZmNmY1ZDM5ZDJkOWRkMDZiNTU0N2MyZDlhNjdmYS9hc3NldHMvbmVvbW1lL3JldHJpZXZhbC1oZWFkcy53ZWJwP3Jhdz10cnVl" alt="Late-interaction and dense retrieval heads for NeoMME" referrerpolicy="no-referrer"> <figcaption>Late-interaction and dense retrieval heads for both <em>NeoMME</em> model sizes.</figcaption> </figure> <blockquote class="note"> <p>Omar Khattab, who introduced late-interaction in ColBERT, <a href="https://x.com/lateinteraction/status/1894696983077785980" rel="nofollow">explains why</a> the term is more precise than "multi-vector." It describes the granularity and learnability of the scoring function, not simply the number of stored vectors.</p> <p>To learn more about late-interaction, we recommend reading <a href="https://meet.ameliechatelain.com/lectures/multi-vector-search/" rel="nofollow">this crash course</a> by Amélie Chatelain.</p> </blockquote> <p>One <em>NeoMME</em>-Retriever forward pass returns both representations, which gives you flexibility no matter your use case and infrastructure. We recommend using late-interaction embeddings in general since they are more powerful and can be used easily with open-source libraries like <a href="https://github.com/lightonai/next-plaid" rel="nofollow">NextPlaid</a>. However, if you have a very large corpora, you can run a single forward pass with <em>NeoMME</em>-Retriever to get the dense embedding, retrieve a small number of documents through an ANN index, and then use late-interaction to rerank the retrieved candidates.</p> <h3 class="relative group flex items-baseline"> <a id="competitive-retrieval-at-compact-model-sizes" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#competitive-retrieval-at-compact-model-sizes" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Competitive retrieval at compact model sizes </span> </h3> <p>We report nDCG@10 on ViDoRe v3. <em>NeoMME</em>-Retriever-260M reaches 0.523, the highest score among evaluated models strictly below 800M parameters. It is within 0.002 nDCG@10 of ColQwen2.5 while using about 14× fewer parameters. <em>NeoMME</em>-Retriever-800M reaches 0.556, within 0.009 nDCG@10 of the similarly sized <a href="https://huggingface.co/vultr/VultronRetrieverFlash-Qwen3.5-0.8B">Vultron Retriever Flash (0.8B)</a>. Both <em>NeoMME</em>-Retriever models lie on the model-size Pareto frontier.</p> <figure> <img src="https://imgproxy.citrusreader.com/VOroGHe4eYCES9FMw5qZAXztHyeBDtosjyblCHXSkLQ/raw:1/aHR0cHM6Ly9naXRodWIuY29tL3Rvbnl3dTcxL2NvbHBhbGktY29va2Jvb2tzL2Jsb2IvYzVlNzM3ZTdlNDc0MzYzODIyYjZmMmQ5NTI5MGMwMTIwZTBmYTk5My9hc3NldHMvbmVvbW1lL3ZpZG9yZS12My1tb2RlbC1zaXplLndlYnA_cmF3PXRydWU" alt="ViDoRe v3 nDCG@10 versus model size" referrerpolicy="no-referrer"> <figcaption>ViDoRe v3 nDCG@10 versus model size.</figcaption> </figure> <p>ViDoRe v1 and v2 use nDCG@5. On both benchmarks, <em>NeoMME</em>-Retriever-260M outperforms ColModernVBERT and the twice-larger ColSmol-500M. <em>NeoMME</em>-Retriever-800M outperforms ColPali v1.3 while using 3.6 times fewer parameters.</p> <table> <caption>Visual document retrieval performance on the ViDoRe benchmarks.</caption> <thead> <tr> <th colspan="2" scope="colgroup">Model details</th> <th colspan="3" scope="colgroup">ViDoRe (nDCG@<em>k</em>)</th> </tr> <tr> <th scope="col">Model</th> <th scope="col">Params.</th> <th scope="col">v3 (@10)</th> <th scope="col">v2 (@5)</th> <th scope="col">v1 (@5)</th> </tr> </thead> <tbody> <tr> <th colspan="5" scope="rowgroup">&lt;300M</th> </tr> <tr> <td><a href="https://huggingface.co/ModernVBERT/colmodernvbert">ColModernVBERT</a></td> <td>250M</td> <td>0.261<sup>†</sup></td> <td>0.407<sup>‡</sup></td> <td>0.806<sup>‡</sup></td> </tr> <tr> <td><a href="https://huggingface.co/vidore/colSmol-256M">ColSmol-256M</a><sup>†</sup></td> <td>256M</td> <td>0.207</td> <td>0.348</td> <td>0.797</td> </tr> <tr style="background-color: rgba(146, 81, 247, 0.12);"> <td><a href="https://huggingface.co/Hcompany/NeoMME-260M-Retriever"><em>NeoMME</em>-260M</a><sup>‡</sup></td> <td>260M</td> <td><strong>0.523</strong></td> <td><strong>0.522</strong></td> <td><strong>0.860</strong></td> </tr> <tr> <th colspan="5" scope="rowgroup">300M to 1B</th> </tr> <tr> <td><a href="https://huggingface.co/vidore/colSmol-500M">ColSmol-500M</a></td> <td>500M</td> <td>0.340<sup>‡</sup></td> <td>0.455<sup>†</sup></td> <td>0.825<sup>†</sup></td> </tr> <tr> <td><a href="https://huggingface.co/vultr/VultronRetrieverFlash-Qwen3.5-0.8B">Vultron Flash</a><sup>†</sup></td> <td>850M</td> <td><strong>0.565</strong></td> <td><strong>0.604</strong></td> <td><strong>0.882</strong></td> </tr> <tr style="background-color: rgba(146, 81, 247, 0.12);"> <td><a href="https://huggingface.co/Hcompany/NeoMME-800M-Retriever"><em>NeoMME</em>-800M</a><sup>‡</sup></td> <td>800M</td> <td>0.556</td> <td>0.559</td> <td>0.874</td> </tr> <tr> <th colspan="5" scope="rowgroup">&gt;1B</th> </tr> <tr> <td><a href="https://huggingface.co/vidore/colqwen2.5-v0.2">ColQwen2.5-v0.2</a><sup>†</sup></td> <td>3.75B</td> <td><strong>0.524</strong></td> <td><strong>0.601</strong></td> <td><strong>0.895</strong></td> </tr> <tr> <td><a href="https://huggingface.co/vidore/colpali-v1.3">ColPali v1.3</a><sup>†</sup></td> <td>2.92B</td> <td>0.430</td> <td>0.547</td> <td>0.848</td> </tr> </tbody> </table> <p><small><sup>†</sup> Scores from MTEB. <sup>‡</sup> Results from our own evaluations.</small></p> <h3 class="relative group flex items-baseline"> <a id="making-high-resolution-retrieval-practical-for-late-interaction" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#making-high-resolution-retrieval-practical-for-late-interaction" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Making high-resolution retrieval practical for late-interaction </span> </h3> <p>Late-interaction storage scales linearly with the number of vectors in the output embedding. Higher-resolution images contain more patches, so they produce larger embeddings. For example, a 2048×2048 square page produces embeddings containing 4,200 vectors with <em>NeoMME</em>-Retriever, or about 2.1 MB in float32. Across the ViDoRe v3 benchmark, the measured average is about 1.5 MB per document.</p> <p>To reduce the storage footprint of the late-interaction index, we combine two complementary compression methods:</p> <ol> <li><a href="https://www.answer.ai/posts/colbert-pooling.html" rel="nofollow">Hierarchical token pooling</a> clusters similar document vectors in a given multi-vector embedding and replaces each cluster with its mean, hence reducing the number of vectors stored for each page.</li> <li><a href="https://www.mixedbread.com/blog/asymmetric-quant" rel="nofollow">Asymmetric quantization</a> quantizes document embeddings to int8 or binary. Because query embeddings are not stored and only generated on-the-fly, they can be kept at a higher precision.</li> </ol> <p>We tested this setup on ViDoRe v3. With a pooling factor 10 and int8 queries and documents, storage decreased from about 1.5 MB to 39 kB per page, a 39× reduction, while keeping more than 99% of the baseline nDCG@10. A more aggressive configuration uses pooling factor 8, int8 queries, and binary documents. That version uses 6 kB per page (255× smaller) and keeps more than 95% of the original retrieval quality.</p> <figure> <img src="https://imgproxy.citrusreader.com/g7RJWodDFccVuIiadtsMXw6EMukYllZsJhX5ICYiH2U/raw:1/aHR0cHM6Ly9naXRodWIuY29tL3Rvbnl3dTcxL2NvbHBhbGktY29va2Jvb2tzL2Jsb2IvNDgxYzZlYzg4NjZmNmY1ZDM5ZDJkOWRkMDZiNTU0N2MyZDlhNjdmYS9hc3NldHMvbmVvbW1lL2NvbXByZXNzaW9uLWZyb250aWVyLTI2MG0ud2VicD9yYXc9dHJ1ZQ" alt="Quality and storage frontier for the NeoMME-260M late-interaction index" referrerpolicy="no-referrer"> <figcaption>Quality and storage frontier for the <em>NeoMME</em>-260M late-interaction index on ViDoRe v3. Labels show pool factor, retained quality, compression, and storage.</figcaption> </figure> <p>Users can pick a compression setting from that frontier based on storage budget and required retrieval quality.</p> <h4 class="relative group flex items-baseline"> <a id="fast-inference-for-cheaper-multimodal-corpus-indexing" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#fast-inference-for-cheaper-multimodal-corpus-indexing" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Fast inference for cheaper multimodal corpus indexing </span> </h4> <p>Before you can search a corpus, a retriever model must turn your documents into embeddings, which will be stored in a vector store like Qdrant, Weaviate, or Milvus. Faster encoding makes building and adding new documents to the index faster, thus reducing the GPU uptime and compute cost required.</p> <p>So we measured image encoding speeds for <em>NeoMME</em>-Retriever against other multimodal document retrievers. We used preprocessed image tensors and calibrated the batch size separately for each model and image size. At a matched 2048×2048 input size on one NVIDIA L40S, <em>NeoMME</em>-Retriever-260M encodes about 51 pages per second, nearly twice ColModernVBERT's 26 pages per second. Both 260M and 800M <em>NeoMME</em>-Retriever models are also faster than the other models we compared on smaller input images.</p> <figure> <img src="https://imgproxy.citrusreader.com/yP_-WhtiHnvfa3WJMHEX-Tyr_yEGNXl_8zZeBBZwNJI/raw:1/aHR0cHM6Ly9naXRodWIuY29tL3Rvbnl3dTcxL2NvbHBhbGktY29va2Jvb2tzL2Jsb2IvYzVlNzM3ZTdlNDc0MzYzODIyYjZmMmQ5NTI5MGMwMTIwZTBmYTk5My9hc3NldHMvbmVvbW1lL2luZGV4aW5nLXRocm91Z2hwdXQtbDQwcy53ZWJwP3Jhdz10cnVl" alt="Document-encoding throughput across image resolutions on an NVIDIA L40S" referrerpolicy="no-referrer"> <figcaption>Document-encoding throughput by retriever and input resolution on one NVIDIA L40S.</figcaption> </figure> <h3 class="relative group flex items-baseline"> <a id="try-neomme-retriever-yourself" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#try-neomme-retriever-yourself" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Try <em>NeoMME</em>-Retriever yourself! </span> </h3> <p><em>NeoMME</em>-Retriever (<a href="https://huggingface.co/Hcompany/NeoMME-260M-Retriever">260M</a> and <a href="https://huggingface.co/Hcompany/NeoMME-800M-Retriever">800M</a>) returns dense and multi-vector embeddings together. The example below scores two text queries against two document-page images with MeanMaxSim late interaction and dense cosine similarity.</p> <details> <summary>Click to see the complete 🤗 <code>transformers</code> example snippet</summary> <pre><code class="language-bash"><span class="hljs-comment"># accelerate is an optional dependency needed only when using device_map="auto".</span> pip install -U accelerate <span class="hljs-string">"transformers @ git+https://github.com/huggingface/transformers.git@main"</span> <span class="hljs-string">"sentence-transformers&gt;=6.0.0"</span> </code></pre> <pre><code class="language-python"><span class="hljs-keyword">from</span> typing <span class="hljs-keyword">import</span> <span class="hljs-type">Any</span>, <span class="hljs-type">Literal</span> <span class="hljs-keyword">import</span> requests <span class="hljs-keyword">import</span> torch <span class="hljs-keyword">from</span> PIL <span class="hljs-keyword">import</span> Image <span class="hljs-keyword">from</span> sentence_transformers.util <span class="hljs-keyword">import</span> cos_sim, mean_maxsim <span class="hljs-keyword">from</span> transformers <span class="hljs-keyword">import</span> BatchFeature, NeoMMEForRetrieval, NeoMMEProcessor <span class="hljs-keyword">def</span> <span class="hljs-title function_">encode</span>(<span class="hljs-params"></span> <span class="hljs-params"> messages: <span class="hljs-built_in">list</span>[<span class="hljs-built_in">list</span>[<span class="hljs-built_in">dict</span>[<span class="hljs-built_in">str</span>, <span class="hljs-type">Any</span>]]],</span> <span class="hljs-params"> task: <span class="hljs-type">Literal</span>[<span class="hljs-string">"query"</span>, <span class="hljs-string">"document"</span>],</span> <span class="hljs-params"></span>) -&gt; BatchFeature: <span class="hljs-keyword">return</span> processor.apply_chat_template( messages, task=task, tokenize=<span class="hljs-literal">True</span>, return_dict=<span class="hljs-literal">True</span>, return_tensors=<span class="hljs-string">"pt"</span>, processor_kwargs={<span class="hljs-string">"padding"</span>: <span class="hljs-string">"longest"</span>}, ) model_name = <span class="hljs-string">"Hcompany/NeoMME-260M-Retriever"</span> processor = NeoMMEProcessor.from_pretrained(model_name) model = NeoMMEForRetrieval.from_pretrained(model_name, device_map=<span class="hljs-string">"auto"</span>) <span class="hljs-comment"># Document images (our corpus)</span> image_urls = [ <span class="hljs-string">"https://github.com/tonywu71/colpali-cookbooks/blob/6ef1332da6bcb48c7ef1f19b25bfa555be7031a8/examples/data/shift_kazakhstan.jpg?raw=true"</span>, <span class="hljs-string">"https://github.com/tonywu71/colpali-cookbooks/blob/6ef1332da6bcb48c7ef1f19b25bfa555be7031a8/examples/data/energy_electricity_generation.jpg?raw=true"</span>, ] documents = [Image.<span class="hljs-built_in">open</span>(requests.get(url, stream=<span class="hljs-literal">True</span>).raw) <span class="hljs-keyword">for</span> url <span class="hljs-keyword">in</span> image_urls] <span class="hljs-comment"># Queries</span> queries = [ <span class="hljs-string">"Quelle partie de la production pétrolière du Kazakhstan provient de champs en mer ?"</span>, <span class="hljs-string">"Which hour of the day had the highest overall electricity generation in 2019?"</span>, ] document_messages = [ [{<span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>, <span class="hljs-string">"content"</span>: [{<span class="hljs-string">"type"</span>: <span class="hljs-string">"image"</span>, <span class="hljs-string">"image"</span>: document}]}] <span class="hljs-keyword">for</span> document <span class="hljs-keyword">in</span> documents ] query_messages = [[{<span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>, <span class="hljs-string">"content"</span>: query}] <span class="hljs-keyword">for</span> query <span class="hljs-keyword">in</span> queries] inputs_documents = encode(document_messages, <span class="hljs-string">"document"</span>).to(model.device) inputs_text = encode(query_messages, <span class="hljs-string">"query"</span>).to(model.device) <span class="hljs-keyword">with</span> torch.inference_mode(): document_outputs = model(**inputs_documents) query_outputs = model(**inputs_text) late_scores = mean_maxsim( query_outputs.embeddings, document_outputs.embeddings, a_mask=inputs_text[<span class="hljs-string">"attention_mask"</span>], b_mask=inputs_documents[<span class="hljs-string">"attention_mask"</span>], ) dense_scores = cos_sim(query_outputs.dense_embeddings, document_outputs.dense_embeddings) <span class="hljs-comment"># Expected: late_scores[0, 0] &gt; late_scores[0, 1] and late_scores[1, 1] &gt; late_scores[1, 0].</span> <span class="hljs-built_in">print</span>(late_scores, dense_scores) </code></pre> </details> <h3 class="relative group flex items-baseline"> <a id="fine-tuning-with-sentence-transformers" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#fine-tuning-with-sentence-transformers" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Fine-tuning with Sentence Transformers </span> </h3> <p>We provide separate <a href="https://huggingface.co/Hcompany/NeoMME-260M-Retriever-ST-dense">dense</a> and <a href="https://huggingface.co/Hcompany/NeoMME-260M-Retriever-ST-late">late-interaction</a> checkpoints for fine-tuning with <a href="https://huggingface.co/blog/multi-vector-encoder">Sentence Transformers v6</a>. Following the same pattern as text encoders such as ModernBERT, Sentence Transformers loads the backbone through <a href="https://huggingface.co/docs/transformers/main/en/model_doc/neomme#transformers.NeoMMEModel"><code>NeoMMEModel</code></a> rather than the dual-head <a href="https://huggingface.co/docs/transformers/main/en/model_doc/neomme#transformers.NeoMMEForRetrieval"><code>NeoMMEForRetrieval</code></a> class. Sentence Transformers currently supports one retrieval head per model, so each checkpoint lets you fine-tune the dense or late-interaction head independently. To train both heads together, use <code>NeoMMEForRetrieval</code> with a custom <code>Trainer</code>.</p> <h3 class="relative group flex items-baseline"> <a id="from-retrieval-to-visual-rag" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#from-retrieval-to-visual-rag" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> From retrieval to visual RAG </span> </h3> <p>Visual document retrieval can be used as the first stage of a visual retrieval-augmented generation (RAG) system. Unlike text RAG, which retrieves extracted text chunks, visual RAG retrieves the original page images and sends them to a visual language model. The model can then use tables, plots, diagrams, and page layout that text extraction may flatten or omit. Here is how visual RAG works:</p> <ol> <li>Indexing: Convert each PDF page to an image, generate an embedding with a retrieval model, and store the embeddings in a vector store.</li> <li>Retrieval: Generate an embedding for the user's query with the same model and retrieve the top-k most relevant pages.</li> <li>Generation: Append the images after the query in the chat message (<em>e.g.</em>, <code>{query}{img_1}{img_2}...{img_k}</code>) and send it to a VLM to generate the answer.</li> </ol> <p>You can test visual RAG directly with <em>NeoMME</em>-Retriever in our HF Space: 🤗 <a href="https://huggingface.co/spaces/tonywu71/neomme-retriever-demo">tonywu71/neomme-retriever-demo</a>.</p> <video controls="" width="640"> <source src="https://raw.githubusercontent.com/tonywu71/colpali-cookbooks/fedbdbfaae4b935993f96fc8fcc8ec7478a80b19/assets/neomme/neomme-retriever-demo.mp4" type="video/mp4"> </video> <h2 class="relative group flex items-baseline"> <a id="conclusion" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#conclusion" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Conclusion </span> </h2> <p><em>NeoMME</em> replaces separate pretrained image and text encoders with one long-context bidirectional Transformer. We train it from scratch to process both multilingual text tokens and raw 32×32 image patches.</p> <p><em>NeoMME</em>-Retriever is a fine-tuned version of <em>NeoMME</em> for visual document retrieval. One forward pass produces both dense and late-interaction representations. The 260M model outperforms all evaluated models strictly below 800M parameters and, at a matched 2048×2048 input size, encodes pages at about 2× ColModernVBERT's throughput. To reduce the large storage footprint of late-interaction embeddings for high-resolution documents, we experimented with hierarchical token pooling and asymmetric quantization and managed to reduce the late-interaction embeddings from roughly 1.5 MB to 6 kB per page, a 255× compression, while retaining more than 95% of the baseline nDCG@10.</p> <p>We release all <em>NeoMME</em> model checkpoints and a day-zero Hugging Face Transformers implementation to allow practitioners to build efficient multimodal and multilingual representation models on top of our work.</p> <h2 class="relative group flex items-baseline"> <a id="acknowledgements" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#acknowledgements" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Acknowledgements </span> </h2> <p><em>NeoMME</em> began as a side quest between two good friends. We worked with limited time and compute, and we decided to share the results so the community can build on them. We thank H Company for supporting the work and providing the compute used to train <em>NeoMME</em>.</p> <h2 class="relative group flex items-baseline"> <a id="citation" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/Hcompany/neomme#citation" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Citation </span> </h2> <pre><code class="language-bibtex">@misc{lac2026neommesingletowermultimodalnativemultilingual, title={NeoMME: A Single-Tower Multimodal-Native Multilingual Foundation Encoder for Efficient Fine-Tuning and Inference}, author={Aurélien Lac and Tony Wu}, year={2026}, eprint={2609.01657}, archivePrefix={arXiv}, primaryClass={cs.IR}, url={https://arxiv.org/abs/2609.01657}, } </code></pre> </div>

2026/9/3
阅读更多

Fine-tuning a 350M Model for Better Structured Outputs in 100 GRPO Steps

<div class="SVELTE_HYDRATER contents" data-target="RepoCodeCopy" data-props="{}"><div></div><!----></div> <div> <!--[0--><a target="_blank" class="btn mb-5 font-sans text-sm no-underline" href="https://github.com/huggingface/blog/blob/main/grpo-with-trl-ifstruct.md">Update on GitHub</a><!--]--></div><!----> <!----> <!--[-1--><!--]--> <div class="relative overflow-clip [overflow-clip-margin:3rem] [&amp;>:last-child]:mb-0"><!----><p><!--[--><!--[0--></p><!--]--><!--]-->This guide is a fully public, inexpensive recipe for making a small model substantially better at structured-output compliance. We fine-tune <a href="https://huggingface.co/LiquidAI/LFM2.5-350M">LFM2.5-350M</a> with Group Relative Policy Optimization (GRPO) using the <a href="https://huggingface.co/docs/trl/en/index">TRL library</a> and evaluate it on the <a 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 href="https://github.com/Liquid4All/cookbook/blob/main/finetuning/notebooks/grpo_with_trl_ifstruct.ipynb" rel="nofollow">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></p> <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> <p><em>Note that the training pipeline described here is not the one used to train the RL model described in the <a href="https://www.liquid.ai/blog/ifstruct-v1.0" rel="nofollow">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> <h2 class="relative group flex items-baseline"> <a id="prerequisites" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/grpo-with-trl-ifstruct#prerequisites" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Prerequisites </span> </h2> <p>This guide has two halves that run in different places:</p> <ul> <li><strong>Fine-tuning</strong> runs on a GPU. The accompanying notebook is sized for a free-tier Colab or Kaggle GPU. </li> <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> </ul> <p>We will need <a href="https://docs.astral.sh/uv/" rel="nofollow"><code>uv</code></a> for the Python tooling and <code>llama.cpp</code> for serving. Following the <a href="https://docs.liquid.ai/deployment/on-device/llama-cpp" rel="nofollow">Liquid AI llama.cpp deployment docs</a>, install <code>llama.cpp</code> with Homebrew and verify that <code>llama-server</code> is available:</p> <pre><code class="language-shell">brew install llama.cpp llama-server --version </code></pre> <h2 class="relative group flex items-baseline"> <a id="ifstruct-evaluation-on-lfm25-350m-base-model" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/grpo-with-trl-ifstruct#ifstruct-evaluation-on-lfm25-350m-base-model" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> IFStruct Evaluation on LFM2.5-350M (Base model) </span> </h2> <p>Before we begin, let's evaluate LFM2.5-350M on the <a 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> <p><strong>IFStruct</strong> is a benchmark for testing the validity of LLM outputs and schema adherence. The benchmark is open-source in <a href="https://github.com/Liquid4All/ifstruct" rel="nofollow">Liquid4All/ifstruct</a>, with the public benchmark dataset available on Hugging Face at <a href="https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0">LiquidAI/ifstruct-v1.0</a>.</p> <pre><code class="language-shell">git clone https://github.com/Liquid4All/ifstruct.git </code></pre> <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 href="https://huggingface.co/LiquidAI/LFM2.5-350M-GGUF">LiquidAI/LFM2.5-350M-GGUF</a>).</p> <p>Then we start the base-model server with the following command:</p> <pre><code class="language-shell">llama-server \ -hf LiquidAI/LFM2.5-350M-GGUF:BF16 \ -c 32768 \ -np 4 \ -ngl 99 \ --alias LiquidAI/LFM2.5-350M \ --host 127.0.0.1 \ --port 8080 </code></pre> <ul> <li><code>--alias</code>: model name IFStruct sends to the OpenAI-compatible endpoint </li> <li><code>-ngl 99</code>: asks <code>llama.cpp</code> to offload all layers to the GPU when available </li> <li><code>-np 4</code>: serves four requests in parallel </li> <li><code>-c 32768</code>: size of the prompt context</li> </ul> <p>Once the server is running, we can run the full benchmark with 2000 samples:</p> <pre><code class="language-shell">uv run ifstruct-eval \ --model LiquidAI/LFM2.5-350M \ --base-url http://localhost:8080/v1 \ --api-key dummy \ --dataset data/test.jsonl \ --results-file results/lfm2.5-350m-llamacpp-base.json \ --n-threads 4 \ --max-tokens 2048 \ -v </code></pre> <pre><code>============================================================ Model: LiquidAI/LFM2.5-350M ============================================================ Overall: 452/2000 passed (22.6%) Average latency: 1453ms By format: JSON: 180/1000 passed (18.0%) YAML: 272/1000 passed (27.2%) By top-level structure: Wrapper key 288/1011 passed (28.5%) Bare list 164/989 passed (16.6%) By entity type: test__camera_review 6/83 passed (7.2%) test__clinical_trial 20/104 passed (19.2%) test__conference_schedule 7/87 passed (8.0%) test__escaping__bug_report_batch 24/89 passed (27.0%) test__escaping__config_snippet_audit 15/85 passed (17.6%) test__escaping__customer_email_thread 5/73 passed (6.8%) test__escaping__dialogue_sample 14/95 passed (14.7%) test__escaping__interview_transcript_segment 21/80 passed (26.2%) test__escaping__log_parser_examples 21/72 passed (29.2%) test__escaping__pr_discussion 22/87 passed (25.3%) test__escaping__repro_steps_batch 16/73 passed (21.9%) test__escaping__screenplay_scene 16/92 passed (17.4%) test__escaping__short_story_chapter 15/84 passed (17.9%) test__escaping__support_ticket_batch 27/73 passed (37.0%) test__escaping__terminal_session_notes 20/70 passed (28.6%) test__event_ticket_booking 49/107 passed (45.8%) test__gpu_review 6/94 passed (6.4%) test__invoice 28/86 passed (32.6%) test__job_posting 25/85 passed (29.4%) test__real_estate_listing 31/82 passed (37.8%) test__recipe 3/70 passed (4.3%) test__rental_car_booking 27/79 passed (34.2%) test__scientific_experiment 13/69 passed (18.8%) test__travel_itinerary 21/81 passed (25.9%) Common errors: 7228x required field missing 738x wrong item count 540x type mismatch 317x Unclosed code block 190x extraneous field 'notes' 181x extraneous field 'path' 175x extraneous field 'constraints' 170x extraneous field 'type' 170x missing code block 100x expected bare list, got wrapper </code></pre> <p>The <a href="https://www.liquid.ai/blog/ifstruct-v1.0" rel="nofollow">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> <h2 class="relative group flex items-baseline"> <a id="grpo-fine-tuning-with-trl-on-structured-outputs" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/grpo-with-trl-ifstruct#grpo-fine-tuning-with-trl-on-structured-outputs" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> GRPO Fine-tuning with TRL on Structured Outputs </span> </h2> <p>The full, runnable pipeline lives in the <a href="https://github.com/Liquid4All/cookbook/blob/main/finetuning/notebooks/grpo_with_trl_ifstruct.ipynb" rel="nofollow">accompanying notebook</a>. We will cover only the relevant pieces in this section.</p> <h3 class="relative group flex items-baseline"> <a id="training-data" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/grpo-with-trl-ifstruct#training-data" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Training data </span> </h3> <p>We use <a 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> <p>Because the Nemotron data distribution differs from the IFStruct evaluation, we augment the prompts to close two gaps between them:</p> <ul> <li><strong>40%</strong> get a "return the output inside a fenced code block" instruction appended, so the model learns to <em>follow</em> the format instruction rather than always emitting raw JSON. </li> <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> </ul> <h3 class="relative group flex items-baseline"> <a id="model-and-lora" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/grpo-with-trl-ifstruct#model-and-lora" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Model and LoRA </span> </h3> <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> <pre><code class="language-py">lora_config = LoraConfig( r=<span class="hljs-number">16</span>, lora_alpha=<span class="hljs-number">32</span>, bias=<span class="hljs-string">"none"</span>, task_type=<span class="hljs-string">"CAUSAL_LM"</span>, target_modules=[ <span class="hljs-string">"q_proj"</span>, <span class="hljs-string">"k_proj"</span>, <span class="hljs-string">"v_proj"</span>, <span class="hljs-string">"out_proj"</span>, <span class="hljs-string">"in_proj"</span>, <span class="hljs-string">"w1"</span>, <span class="hljs-string">"w2"</span>, <span class="hljs-string">"w3"</span>, ], ) </code></pre> <p>This trains ~6M parameters, about 1.66% of the model.</p> <h3 class="relative group flex items-baseline"> <a id="reward-functions" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/grpo-with-trl-ifstruct#reward-functions" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Reward functions </span> </h3> <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> <ul> <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> <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> <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> </ul> <p>We combine the three as a weighted sum with <code>reward_weights=[1.0, 0.5, 2.0]</code>.</p> <h3 class="relative group flex items-baseline"> <a id="training" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/grpo-with-trl-ifstruct#training" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Training </span> </h3> <p>We train for 100 steps with 8 generations per prompt group, sized for a free-tier 16 GB GPU:</p> <pre><code class="language-py"><span class="hljs-keyword">from</span> trl <span class="hljs-keyword">import</span> GRPOConfig training_args = GRPOConfig( output_dir=<span class="hljs-string">"./outputs/lfm25-350m-nemotron-schema-grpo"</span>, learning_rate=<span class="hljs-number">5e-5</span>, max_steps=<span class="hljs-number">100</span>, warmup_steps=<span class="hljs-number">10</span>, num_generations=<span class="hljs-number">8</span>, <span class="hljs-comment"># completions sampled per prompt group</span> per_device_train_batch_size=<span class="hljs-number">4</span>, gradient_accumulation_steps=<span class="hljs-number">8</span>, <span class="hljs-comment"># 4 prompt groups per optimizer step</span> steps_per_generation=<span class="hljs-number">2</span>, max_completion_length=<span class="hljs-number">1024</span>, <span class="hljs-comment"># room for nested JSON</span> mask_truncated_completions=<span class="hljs-literal">False</span>, temperature=<span class="hljs-number">1.1</span>, <span class="hljs-comment"># hotter sampling keeps groups varied</span> beta=<span class="hljs-number">0.01</span>, <span class="hljs-comment"># KL penalty toward the reference model</span> reward_weights=[<span class="hljs-number">1.0</span>, <span class="hljs-number">0.5</span>, <span class="hljs-number">2.0</span>], <span class="hljs-comment"># json_format, field_count, schema_validation</span> logging_steps=<span class="hljs-number">1</span>, save_steps=<span class="hljs-number">100</span>, ) </code></pre> <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> <h3 class="relative group flex items-baseline"> <a id="merging-and-saving-the-model" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/grpo-with-trl-ifstruct#merging-and-saving-the-model" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Merging and saving the model </span> </h3> <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> <pre><code class="language-py">MERGED_DIR = <span class="hljs-string">f"<span class="hljs-subst">{training_args.output_dir}</span>-merged"</span> merged_model = trainer.model.merge_and_unload() merged_model.save_pretrained(MERGED_DIR) tokenizer.save_pretrained(MERGED_DIR) </code></pre> <h2 class="relative group flex items-baseline"> <a id="ifstruct-evaluation-on-grpo-tuned-lfm25-350m" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/grpo-with-trl-ifstruct#ifstruct-evaluation-on-grpo-tuned-lfm25-350m" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> IFStruct Evaluation on GRPO Tuned LFM2.5-350M </span> </h2> <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> <pre><code class="language-shell">git clone --depth 1 https://github.com/ggml-org/llama.cpp pip install ./llama.cpp/gguf-py mkdir -p models python llama.cpp/convert_hf_to_gguf.py \ PATH_TO_YOUR_MERGED_MODEL \ --outfile ./models/lfm25-350m-grpo-bf16.gguf \ --outtype bf16 </code></pre> <p>Then we serve the merged model with the following command:</p> <pre><code class="language-shell">llama-server \ -m ./models/lfm25-350m-grpo-bf16.gguf \ --alias lfm25-350m-grpo-structured-output \ -c 32768 \ -np 4 \ -ngl 99 \ --host 127.0.0.1 \ --port 8081 </code></pre> <p>Then, we will run the full IFStruct evaluation again with the fine-tuned model:</p> <pre><code class="language-shell">uv run ifstruct-eval \ --model lfm25-350m-grpo-structured-output \ --base-url http://localhost:8081/v1 \ --api-key dummy \ --dataset data/test.jsonl \ --results-file results/lfm25-350m-grpo.json \ --n-threads 4 \ --max-tokens 2048 \ -v </code></pre> <pre><code>============================================================ Model: lfm25-350m-grpo-structured-output ============================================================ Overall: 594/2000 passed (29.7%) Average latency: 1518ms By format: JSON: 319/1000 passed (31.9%) YAML: 275/1000 passed (27.5%) By top-level structure: Wrapper key 300/1011 passed (29.7%) Bare list 294/989 passed (29.7%) By entity type: test__camera_review 5/83 passed (6.0%) test__clinical_trial 31/104 passed (29.8%) test__conference_schedule 11/87 passed (12.6%) test__escaping__bug_report_batch 32/89 passed (36.0%) test__escaping__config_snippet_audit 24/85 passed (28.2%) test__escaping__customer_email_thread 9/73 passed (12.3%) test__escaping__dialogue_sample 17/95 passed (17.9%) test__escaping__interview_transcript_segment 13/80 passed (16.2%) test__escaping__log_parser_examples 33/72 passed (45.8%) test__escaping__pr_discussion 26/87 passed (29.9%) test__escaping__repro_steps_batch 23/73 passed (31.5%) test__escaping__screenplay_scene 34/92 passed (37.0%) test__escaping__short_story_chapter 24/84 passed (28.6%) test__escaping__support_ticket_batch 36/73 passed (49.3%) test__escaping__terminal_session_notes 23/70 passed (32.9%) test__event_ticket_booking 62/107 passed (57.9%) test__gpu_review 7/94 passed (7.4%) test__invoice 36/86 passed (41.9%) test__job_posting 33/85 passed (38.8%) test__real_estate_listing 32/82 passed (39.0%) test__recipe 7/70 passed (10.0%) test__rental_car_booking 37/79 passed (46.8%) test__scientific_experiment 14/69 passed (20.3%) test__travel_itinerary 25/81 passed (30.9%) Common errors: 7331x required field missing 890x wrong item count 555x type mismatch 102x expected bare list, got wrapper 62x extraneous field 'metadata.tone' 55x 6 is greater than maximum 5 49x extraneous field 'speaker_labels' 47x extraneous field 'tone' 44x 'cups' not in allowed values ['mg', 'g', 'kg', 'oz', 'lb', 'ml', 'l', 'cl', 'dl' 44x extraneous field 'notes' </code></pre> <p>Comparing the two runs on the identical serving stack:</p> <div class="max-w-full overflow-auto"> <table> <thead><tr> <th align="left">IFStruct group</th> <th align="left">base</th> <th align="left">GRPO-tuned</th> <th align="left">Δ</th> </tr> </thead><tbody><tr> <td align="left"><strong>Overall</strong></td> <td align="left">22.6%</td> <td align="left"><strong>29.7%</strong></td> <td align="left"><strong>+7.1</strong></td> </tr> <tr> <td align="left">JSON</td> <td align="left">18.0%</td> <td align="left">31.9%</td> <td align="left">+13.9</td> </tr> <tr> <td align="left">YAML</td> <td align="left">27.2%</td> <td align="left">27.5%</td> <td align="left">+0.3</td> </tr> <tr> <td align="left">Wrapper key</td> <td align="left">28.5%</td> <td align="left">29.7%</td> <td align="left">+1.2</td> </tr> <tr> <td align="left">Bare list</td> <td align="left">16.6%</td> <td align="left">29.7%</td> <td align="left">+13.1</td> </tr> </tbody> </table> </div> <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 href="https://www.liquid.ai/blog/ifstruct-v1.0" rel="nofollow">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> <h2 class="relative group flex items-baseline"> <a id="conclusion" class="block pr-1.5 text-lg md:absolute md:p-1.5 md:opacity-0 md:group-hover:opacity-100 md:right-full" href="https://huggingface.co/blog/grpo-with-trl-ifstruct#conclusion" rel="nofollow"> <span class="header-link"><svg class="text-gray-500 hover:text-black dark:hover:text-gray-200 w-4" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path d="M167.594 88.393a8.001 8.001 0 0 1 0 11.314l-67.882 67.882a8 8 0 1 1-11.314-11.315l67.882-67.881a8.003 8.003 0 0 1 11.314 0zm-28.287 84.86l-28.284 28.284a40 40 0 0 1-56.567-56.567l28.284-28.284a8 8 0 0 0-11.315-11.315l-28.284 28.284a56 56 0 0 0 79.196 79.197l28.285-28.285a8 8 0 1 0-11.315-11.314zM212.852 43.14a56.002 56.002 0 0 0-79.196 0l-28.284 28.284a8 8 0 1 0 11.314 11.314l28.284-28.284a40 40 0 0 1 56.568 56.567l-28.285 28.285a8 8 0 0 0 11.315 11.314l28.284-28.284a56.065 56.065 0 0 0 0-79.196z" fill="currentColor"></path></svg></span> </a> <span> Conclusion </span> </h2> <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> <p>To reproduce or extend this work, see the original <a href="https://www.liquid.ai/blog/ifstruct-v1.0" rel="nofollow">IFStruct v1.0 blog post</a>, the <a href="https://github.com/Liquid4All/ifstruct" rel="nofollow">Liquid4All/ifstruct</a> benchmark repo, and the <a href="https://huggingface.co/datasets/LiquidAI/ifstruct-v1.0">LiquidAI/ifstruct-v1.0</a> dataset.</p> <!----></div>

2026/9/3
阅读更多

推荐订阅

| 精品推荐 | 技术干货 | 职场成长 | 社区动态 | 组件物料 | 行业动态 | | --------- | -------- | -------- | --------- | -------- | -------- | | recommend | quality | growth | community | material | industry |

::: tip 订阅 [管理与运维](https://tidb.net/blog/c/management-and-operation),其源网址为 `https://tidb.net/blog/c/management-and-operation`,请参考该 URL 指定部分构成参数,此时路由为 [`/tidb/blog/c/management-and-operation`](https://rsshub.app/tidb/blog/c/management-and-operation)。 ::: | 分类 | ID | | -------------------------------------------------------------- | ----------------------------------------------------------------------------------- | | [全部文章](https://tidb.net/blog) | [latest](https://rsshub.app/tidb/blog) | | [管理与运维](https://tidb.net/blog/c/management-and-operation) | [management-and-operation](https://rsshub.app/tidb/blog/c/management-and-operation) | | [实践案例](https://tidb.net/blog/c/practical-case) | [practical-case](https://rsshub.app/tidb/blog/c/practical-case) | | [架构选型](https://tidb.net/blog/c/architecture-selection) | [architecture-selection](https://rsshub.app/tidb/blog/c/architecture-selection) | | [原理解读](https://tidb.net/blog/c/principle-interpretation) | [principle-interpretation](https://rsshub.app/tidb/blog/c/principle-interpretation) | | [应用开发](https://tidb.net/blog/c/application-development) | [application-development](https://rsshub.app/tidb/blog/c/application-development) | | [社区动态](https://tidb.net/blog/c/community-feeds) | [community-feeds](https://rsshub.app/tidb/blog/c/community-feeds) |