unsloth/unsloth/models/rl_replacements.py
Daniel Han 07272b9278
Experimental: correct varlen sample packing for hybrid linear-attention models (#7249)
* Fix text-only VLM CPT packing truncation

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Handle streaming vision datasets in packing

* Harden multimodal packing detection

* Preserve safe packing boundaries

* Scope stream packing checks to VLMs

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Narrow VLM packing detection

* Align packing mode and eval safety

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Add qwen3_5/qwen3_next to PADDING_FREE_BLOCKLIST to avoid packed-sequence contamination

* Detect hybrid linear-attention models structurally instead of by name for packing guard

* Add experimental varlen packing for hybrid linear-attention models

Feed seq_idx to the causal conv and cu_seqlens to the gated-delta scan so
sample packing / padding-free reset state at sequence boundaries for hybrid
linear-attention models (Qwen3.5, Qwen3-Next). Gated behind
UNSLOTH_EXPERIMENTAL_HYBRID_PACKING and fail-closed: when the flag is off or
the accelerated kernels (causal_conv1d + fla) are unavailable, the guard keeps
these models on the padded path.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Harden hybrid linear-attention varlen packing shim

Make patch_hybrid_linear_attention_varlen robust across transformers 4.57.6
through 5.x and TRL 0.22.2 through 1.x, following the import_fixes.py style:

- Read UNSLOTH_EXPERIMENTAL_HYBRID_PACKING at call time so the flag takes effect
  when set after importing unsloth.
- Idempotent: repeat calls on a patched model return True without re-validating
  the wrappers or double-wrapping; signatures are checked on captured originals.
- Prefer the authoritative packed_seq_lengths (via get_packed_info_from_kwargs)
  over position_ids resets, handling pad_to_multiple_of trailing tokens.
- Suppress injection for cached forwards (use_cache / past_key_values) so
  generation and eval are left on the untouched decode path.
- Validate every gated-delta module before mutating any (transactional).
- Bind position_ids / use_cache from both positional and keyword args.
- Verify dispatch at runtime (Unsloth wraps each module forward, so the mixer
  source is not statically inspectable) and warn once if the shim is never hit.
- Emit one deduped diagnostic on each fail-closed path.

Add CPU unit tests covering the hybrid guard detection, the boundary builders,
and the shim (fail-closed, active, idempotent, cached no-op, runtime handshake).

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Abort hybrid packing when the varlen shim is not fully dispatched

The runtime handshake used a single per-module hit flag written by both the conv
and scan wrappers, so a partial dispatch (only one kernel routed through
self.<kernel>) passed the any() check and trained on contaminated data, and a
missing dispatch only logged a warning. Track conv and scan dispatch separately,
require both on every gated-delta module on the first packed forward, and raise
before loss/backward when either is missing (the batch is already flattened, so
there is no padded recovery at that point). Also skip an empty packed_seq_lengths
before it reaches max(), and document the position_ids fallback's left-pad
assumption.

Add tests for no-dispatch and partial (conv-only / scan-only) abort, the
packed_seq_lengths preference over a competing position_ids, MRoPE 3D position
ids, and the pad_to_multiple_of trailing-segment path through the metadata builder.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Import the hybrid packing patch from its submodule to satisfy the import-hoist lint

* Fail closed for hybrid packing on encoder-decoder, chunked-loss, and string-name models

The varlen shim only helps decoder-only hybrid models that run their mixer
through self.<kernel> on a live nn.Module forward. Three cases slipped past
the guard:

- Encoder-decoder configs (is_encoder_decoder) reached the packing path even
  though flattening a cross-attention batch is unsound. Block them explicitly.
- TRL's chunked_nll loss (the 1.x default) calls the backbone directly and
  bypasses model.forward, so the per-instance forward wrapper that refreshes
  the varlen stash never runs. Detect that path and keep the model padded.
- A string model_name reaches the trainer before the module exists, so the
  instance shim has nothing to patch. Resolve the config up front and keep
  string hybrids on the padded path.

Adds encoder-decoder / decoder-only / chunked-loss / string-model tests.

* Harden the SFT source-injection replacements and forward auth args for string models

The wrapped-packing injection rewrote the sourced unsloth_zoo sft_prepare_dataset
with str.replace anchored on the exact 'All Unsloth Zoo code licensed under
LGPLv3' comment. str.replace never raises on a missing anchor, so a supported
newer unsloth_zoo (the dependency is only lower-bounded) that moved that header
would silently drop the setup while the truncation and pack_dataset edits still
referenced _unsloth_wrapped_packing / _inspect, raising NameError on every SFT
dataset preparation.

- Install the setup at the sft_prepare_dataset signature via re.subn (a structural
  anchor that always exists) and raise if even that is missing.
- Route the remaining edits through a _require_replace helper that fails loudly on a
  missing required anchor (or warns once for an optional one), formalizing the
  verify-then-replace idiom the DPO patchers in this file already use.
- Reuse the guarded _unsloth_pack_has_strategy at the pack_dataset call instead of
  re-calling inspect.signature(pack_dataset) unguarded, so a non-introspectable
  pack_dataset cannot crash there after the setup already handled it.
- _resolve_string_model_config now forwards token / use_auth_token / cache_dir /
  code_revision, so a private hybrid resolves its config instead of falling through
  as non-hybrid and enabling packing without the varlen shim.

Adds regression tests for the drift-resistant injection, the helper, and the
string-model auth forwarding.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Honor top-level SFTConfig.trust_remote_code when resolving a string model

TRL merges the top-level args.trust_remote_code into the load via
model_init_kwargs.setdefault("trust_remote_code", args.trust_remote_code) before
create_model_from_path, so a remote-code hybrid is commonly set with
SFTConfig(trust_remote_code=True) rather than inside model_init_kwargs. The config
probe only read model_init_kwargs, so AutoConfig could fail for such a model, leave
model_config None, and let the guard treat it as non-hybrid, enabling packing
without the varlen shim. Mirror TRL's setdefault (model_init_kwargs wins).

* Tighten hybrid-packing comments for concision

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: alkinun <alkinunl@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Etherl <61019402+Etherll@users.noreply.github.com>
2026-07-20 00:57:02 -07:00

2944 lines
136 KiB
Python

# Copyright 2023-present Daniel Han-Chen & the Unsloth team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__all__ = [
"RL_EXTRA_ARGS",
"RL_FUNCTIONS",
"RL_PRE_ITEMS",
"RL_CONFIG_CHANGES",
"RL_METRICS_CHANGES",
]
import os
import re
import torch
import inspect
import linecache
from collections import defaultdict
from unsloth_zoo.rl_replacements import (
RL_REPLACEMENTS,
left_pack_padding,
create_completion_attention_mask,
chunked_selective_log_softmax,
chunked_hidden_states_selective_log_softmax,
_unsloth_get_mm_token_id,
_unsloth_fix_mm_token_type_ids,
)
from unsloth_zoo.utils import Version
from trl import __version__ as trl_version_raw
from importlib.metadata import version as importlib_version
from unsloth_zoo.log import logger
from unsloth_zoo.device_type import device_synchronize
import importlib.util
from ..device_type import (
is_hip,
get_device_type,
DEVICE_TYPE,
DEVICE_TYPE_TORCH,
DEVICE_COUNT,
ALLOW_PREQUANTIZED_MODELS,
)
import textwrap
from ._utils import _get_inference_mode_context_manager, UNSLOTH_ENABLE_LOGGING
# One-time GRPO sequence-packing gates; mirrored into the generated trainer cache via RL_PRE_ITEMS.
UNSLOTH_GRPO_SEQ_PACKING_ON = os.environ.get("UNSLOTH_GRPO_SEQ_PACKING", "1").lower() not in (
"0",
"false",
"no",
"off",
)
# Packing needs zoo#840's masked-column guard in grpo_compute_loss (installed zoo is fixed per-process).
try:
UNSLOTH_ZOO_HAS_MASKED_COL_GUARD = "torch.where(_keep, new" in inspect.getsource(
RL_REPLACEMENTS["grpo_compute_loss"]
)
except Exception:
UNSLOTH_ZOO_HAS_MASKED_COL_GUARD = False
# One-time PrefixGrouper gate; any import failure degrades to "PrefixGrouper off".
_pg_build_layout = _pg_enabled_fn = _pg_verify_on = _pg_tol_ok = _PG_TOL_KILL = None
UNSLOTH_GRPO_PREFIX_GROUPER_ON = os.environ.get("UNSLOTH_GRPO_PREFIX_GROUPER", "1").lower() not in (
"0",
"false",
"no",
"off",
)
if UNSLOTH_GRPO_PREFIX_GROUPER_ON:
try:
from ..utils.prefix_grouper import (
build_group_layout as _pg_build_layout,
prefix_grouper_enabled as _pg_enabled_fn,
verify_on as _pg_verify_on,
tol_ok as _pg_tol_ok,
TOL_KILL as _PG_TOL_KILL,
)
except Exception:
UNSLOTH_GRPO_PREFIX_GROUPER_ON = False
RL_EXTRA_ARGS = defaultdict(list)
RL_FUNCTIONS = defaultdict(list)
RL_PRE_ITEMS = defaultdict(list)
def _unsloth_clear_stateful_mrope(model):
modules = getattr(model, "modules", None)
if modules is None:
return False
cleared = False
for module in modules():
if hasattr(module, "compute_3d_position_ids") and hasattr(module, "rope_deltas"):
module.rope_deltas = None
cleared = True
return cleared
RL_CONFIG_CHANGES = defaultdict(list)
RL_METRICS_CHANGES = defaultdict(list)
RL_ADDITIONAL_FUNCTIONS = defaultdict(list)
_DPO_VISION_KEYS = (
"pixel_position_ids",
"image_position_ids",
"mm_token_type_ids",
)
torch_compile_options = {
"epilogue_fusion": True,
"max_autotune": False, # I saw speedups, but not sure if this has issues in collab
"shape_padding": True,
"trace.enabled": False,
"triton.cudagraphs": False,
}
try:
trl_version = Version(trl_version_raw)
except Exception:
try:
trl_version = Version(importlib_version("trl"))
except Exception:
trl_version = Version("0.0.0")
# Check untrained tokens
def sft_trainer_fix_untrained_tokens(call_args, extra_args):
if "model" in call_args and "train_dataset" in call_args:
fix_tokenizer = (
"IGNORED_TOKENIZER_NAMES = os.environ.get('UNSLOTH_IGNORED_TOKENIZER_NAMES', '').split('\\n')\n"
"from unsloth_zoo.tokenizer_utils import fix_untrained_tokens\n"
"from unsloth_zoo.training_utils import fix_zero_training_loss\n"
"if 'tokenizer' not in locals(): tokenizer = processing_class\n"
"fix_untrained_tokens(model, tokenizer, train_dataset, IGNORED_TOKENIZER_NAMES, eps = 1e-16)\n"
"fix_zero_training_loss(model, tokenizer, train_dataset)\n"
)
return fix_tokenizer
return ""
RL_EXTRA_ARGS["sft_trainer"].append(sft_trainer_fix_untrained_tokens)
# Fix top_k for GRPO vLLM.
# https://github.com/huggingface/trl/pull/4695 with this change trl added top_k in GRPOConfig and defaults to 0
# We don't want that since vllm's all include top_k is -1 and 0 returns an error on SamplingParams creation.
def grpo_config_fix_vllm_top_k(old_RLTrainer_source, old_RLConfig_source):
return "if use_vllm and (top_k is None or top_k == 0): top_k = -1\n"
RL_CONFIG_CHANGES["grpo_trainer"].append(grpo_config_fix_vllm_top_k)
# Remove DPO columns which might randomnly be tokenized
def dpo_trainer_fix_columns(call_args, extra_args):
if "model" in call_args and "train_dataset" in call_args:
fix_dpo = (
"if hasattr(train_dataset, 'column_names'):\n"
" column_names = set(train_dataset.column_names)\n"
" check = ['chosen', 'rejected', 'prompt', 'chosen_input_ids', 'chosen_attention_mask',\n"
" 'chosen_labels', 'rejected_input_ids', 'rejected_attention_mask', 'rejected_labels',\n"
" 'prompt_input_ids', 'prompt_attention_mask']\n"
" if all(x in column_names for x in check):\n"
" train_dataset = train_dataset.remove_columns(['chosen', 'rejected', 'prompt'])\n"
" del check, column_names\n"
)
return fix_dpo
return ""
RL_EXTRA_ARGS["dpo_trainer"].append(dpo_trainer_fix_columns)
def dpo_trainer_fix_data_collator(call_args, extra_args):
if (
"data_collator" in call_args
and "train_dataset" in call_args
and "processing_class" in call_args
):
fix_collator = (
"if hasattr(train_dataset, 'column_names'):\n"
" column_names = set(train_dataset.column_names)\n"
" is_dpo_dataset = ({'chosen', 'rejected'}.issubset(column_names) or\n"
" {'prompt_input_ids', 'chosen_input_ids', 'rejected_input_ids'}.issubset(column_names))\n"
" if is_dpo_dataset and isinstance(data_collator, TransformersDataCollatorForLanguageModeling):\n"
" data_collator = None\n"
" del is_dpo_dataset, column_names\n"
)
return fix_collator
return ""
RL_EXTRA_ARGS["dpo_trainer"].append(dpo_trainer_fix_data_collator)
def dpo_trainer_vision_process_row(
features,
processing_class,
max_prompt_length = None,
max_completion_length = None,
add_special_tokens = True,
is_chat = False,
):
text = features.get("prompt", "")
images = features.get("images")
processor, tokenizer = processing_class, processing_class.tokenizer
processed_features = processor(
images = images,
text = text,
add_special_tokens = False,
)
prompt_input_ids = processed_features["input_ids"][0]
chosen_input_ids = tokenizer(features["chosen"], add_special_tokens = False)["input_ids"]
rejected_input_ids = tokenizer(features["rejected"], add_special_tokens = False)["input_ids"]
if add_special_tokens:
if tokenizer.bos_token_id is not None:
prompt_input_ids = [tokenizer.bos_token_id] + prompt_input_ids
if tokenizer.eos_token_id is not None:
prompt_input_ids = prompt_input_ids + [tokenizer.eos_token_id]
if not is_chat and tokenizer.eos_token_id is not None:
chosen_input_ids = chosen_input_ids + [tokenizer.eos_token_id]
rejected_input_ids = rejected_input_ids + [tokenizer.eos_token_id]
if max_prompt_length is not None:
prompt_input_ids = prompt_input_ids[-max_prompt_length:]
if max_completion_length is not None:
chosen_input_ids = chosen_input_ids[:max_completion_length]
rejected_input_ids = rejected_input_ids[:max_completion_length]
output = {
"prompt_input_ids": prompt_input_ids,
"chosen_input_ids": chosen_input_ids,
"rejected_input_ids": rejected_input_ids,
}
if "pixel_values" in processed_features:
output["pixel_values"] = processed_features["pixel_values"][0]
if "pixel_attention_mask" in processed_features:
output["pixel_attention_mask"] = processed_features["pixel_attention_mask"][0]
if "image_sizes" in processed_features:
output["image_sizes"] = processed_features["image_sizes"][0]
if "token_type_ids" in processed_features:
token_type_ids = processed_features["token_type_ids"][0]
if max_prompt_length is not None:
token_type_ids = token_type_ids[-max_prompt_length:]
output["token_type_ids"] = token_type_ids
if "pixel_position_ids" in processed_features:
output["pixel_position_ids"] = processed_features["pixel_position_ids"][0]
if "image_position_ids" in processed_features:
output["image_position_ids"] = processed_features["image_position_ids"][0]
if "mm_token_type_ids" in processed_features:
mm_token_type_ids = processed_features["mm_token_type_ids"][0]
if max_prompt_length is not None:
mm_token_type_ids = mm_token_type_ids[-max_prompt_length:]
output["mm_token_type_ids"] = mm_token_type_ids
return output
def dpo_trainer_vision_signature_columns(function_name, function):
if function_name != "_set_signature_columns_if_needed":
return function
if all(_k in function for _k in _DPO_VISION_KEYS):
return function
_extra_columns = "".join(f' "{_k}",\n' for _k in _DPO_VISION_KEYS)
new_function = function.replace(
' "image_sizes",\n "token_type_ids",\n',
f' "image_sizes",\n{_extra_columns} "token_type_ids",\n',
)
if new_function != function:
return new_function
return function.replace(
' "image_sizes",\n "ref_chosen_logps",\n',
f' "image_sizes",\n{_extra_columns} "ref_chosen_logps",\n',
)
def dpo_trainer_concatenated_inputs(function_name, function):
if function_name != "concatenated_inputs":
return function
if all(_k in function for _k in _DPO_VISION_KEYS):
return function
_extra_inputs = "".join(
f' if "{_k}" in batch:\n'
f' output["{_k}"] = torch.cat((batch["{_k}"], batch["{_k}"]), dim=0)\n'
for _k in _DPO_VISION_KEYS
)
image_sizes_block = (
' if "image_sizes" in batch:\n'
' output["image_sizes"] = torch.cat([batch["image_sizes"], batch["image_sizes"]], dim=0)\n'
)
new_function = function.replace(
image_sizes_block + ' if "token_type_ids" in batch:\n',
image_sizes_block + _extra_inputs + ' if "token_type_ids" in batch:\n',
)
if new_function != function:
return new_function
if image_sizes_block in function:
return function.replace(image_sizes_block, image_sizes_block + _extra_inputs, 1)
return function
def _dpo_trainer_extend_vision_model_kwargs(function):
if all(_k in function for _k in _DPO_VISION_KEYS):
return function
_extra_forward = "".join(
f' if "{_k}" in concatenated_batch:\n'
f' model_kwargs["{_k}"] = concatenated_batch["{_k}"]\n'
for _k in (
"pixel_values",
"pixel_attention_mask",
"image_sizes",
*_DPO_VISION_KEYS,
)
)
return function.replace(
' if "pixel_values" in concatenated_batch:\n'
' model_kwargs["pixel_values"] = concatenated_batch["pixel_values"]\n'
' if "pixel_attention_mask" in concatenated_batch:\n'
' model_kwargs["pixel_attention_mask"] = concatenated_batch["pixel_attention_mask"]\n'
' if "image_sizes" in concatenated_batch:\n'
' model_kwargs["image_sizes"] = concatenated_batch["image_sizes"]\n',
f"{_extra_forward}",
)
def dpo_trainer_concatenated_forward(function_name, function):
if function_name != "concatenated_forward":
return function
return _dpo_trainer_extend_vision_model_kwargs(function)
def dpo_trainer_compute_loss_liger(function_name, function):
if function_name != "_compute_loss_liger":
return function
return _dpo_trainer_extend_vision_model_kwargs(function)
def dpo_trainer_data_collator_vision_keys(call_args, extra_args):
if "data_collator" not in call_args:
return ""
_vision_keys = str(_DPO_VISION_KEYS)
return (
"from trl.trainer.dpo_trainer import DataCollatorForPreference\n"
"if not hasattr(DataCollatorForPreference, '_unsloth_vision_keys_patch'):\n"
" _old_dpo_collator_torch_call = DataCollatorForPreference.torch_call\n"
"\n"
" def _unsloth_dpo_torch_call(self, examples):\n"
" output = _old_dpo_collator_torch_call(self, examples)\n"
" import torch as _unsloth_torch\n"
" try:\n"
" from trl.trainer.utils import pad as _unsloth_trl_pad\n"
" except Exception:\n"
" _unsloth_trl_pad = None\n"
" for _k in " + _vision_keys + ":\n"
" if not all(_k in example for example in examples):\n"
" continue\n"
" _is_position_key = _k.endswith('position_ids')\n"
" _padding_value = -1 if _is_position_key else 0\n"
" _padding_side = 'right' if _is_position_key else 'left'\n"
" _values = [_unsloth_torch.as_tensor(example[_k]) for example in examples]\n"
" try:\n"
" if _unsloth_trl_pad is not None:\n"
" output[_k] = _unsloth_trl_pad(_values, padding_value=_padding_value, padding_side=_padding_side)\n"
" else:\n"
" from torch.nn.utils.rnn import pad_sequence as _unsloth_pad_sequence\n"
" output[_k] = _unsloth_pad_sequence(_values, batch_first=True, padding_value=_padding_value)\n"
" except Exception:\n"
" from torch.nn.utils.rnn import pad_sequence as _unsloth_pad_sequence\n"
" output[_k] = _unsloth_pad_sequence(_values, batch_first=True, padding_value=_padding_value)\n"
" return output\n"
"\n"
" DataCollatorForPreference.torch_call = _unsloth_dpo_torch_call\n"
" DataCollatorForPreference._unsloth_vision_keys_patch = True\n"
)
def dpo_trainer_prepare_dataset(function_name, function):
if function_name != "_prepare_dataset":
return function
legacy_call = "self.tokenize_row if not self.is_vision_model else self.process_row"
if legacy_call not in function:
return function
function = function.replace(
legacy_call,
"self.tokenize_row if not self.is_vision_model else dpo_trainer_vision_process_row",
)
legacy_tokenize_block = (
" # Tokenize the dataset\n"
" if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc`\n"
' map_kwargs["desc"] = f"Tokenizing {dataset_name} dataset"\n'
"\n"
" dataset = dataset.map(\n"
" self.tokenize_row if not self.is_vision_model else dpo_trainer_vision_process_row,\n"
)
patched_tokenize_block = (
" # Tokenize the dataset\n"
" if isinstance(dataset, Dataset): # `IterableDataset.map` does not support `desc`\n"
' map_kwargs["desc"] = f"Tokenizing {dataset_name} dataset"\n'
" if self.is_vision_model:\n"
' map_kwargs.pop("num_proc", None)\n'
"\n"
" dataset = dataset.map(\n"
" self.tokenize_row if not self.is_vision_model else dpo_trainer_vision_process_row,\n"
)
if legacy_tokenize_block in function:
function = function.replace(legacy_tokenize_block, patched_tokenize_block, 1)
return function
RL_FUNCTIONS["dpo_trainer"].append(dpo_trainer_prepare_dataset)
RL_PRE_ITEMS["dpo_trainer"].append(inspect.getsource(dpo_trainer_vision_process_row))
RL_FUNCTIONS["dpo_trainer"].append(dpo_trainer_vision_signature_columns)
RL_FUNCTIONS["dpo_trainer"].append(dpo_trainer_concatenated_inputs)
RL_FUNCTIONS["dpo_trainer"].append(dpo_trainer_concatenated_forward)
RL_FUNCTIONS["dpo_trainer"].append(dpo_trainer_compute_loss_liger)
RL_EXTRA_ARGS["dpo_trainer"].append(dpo_trainer_data_collator_vision_keys)
_WRAPPED_PACKING_SETUP = (
" import inspect as _inspect\n"
" try:\n"
' _unsloth_pack_has_strategy = "strategy" in _inspect.signature(pack_dataset).parameters\n'
" except Exception:\n"
" _unsloth_pack_has_strategy = True\n"
" _unsloth_wrapped_packing = packing and (\n"
' getattr(args, "packing_strategy", None) == "wrapped"\n'
" or not _unsloth_pack_has_strategy\n"
" )\n"
)
_WARNED_MISSING_ANCHORS = set()
def _require_replace(
function,
old,
new,
*,
count = 1,
required = True,
where = "",
):
"""str.replace that never silently no-ops a load-bearing source edit.
Plain str.replace returns the source unchanged when the anchor is absent, so a
drifted anchor in a newer TRL / unsloth_zoo would skip the edit while later edits
still reference helper variables it should have introduced (NameError at runtime).
Fail loudly for a required edit, warn once and skip for an optional one, so a
drifted source can never corrupt the patched function silently.
"""
if old not in function:
detail = f" ({where})" if where else ""
if required:
raise RuntimeError(
f"Unsloth: source anchor not found{detail}; the patched function is out "
"of sync with this TRL / unsloth_zoo version. Please file a bug report."
)
if where not in _WARNED_MISSING_ANCHORS:
_WARNED_MISSING_ANCHORS.add(where)
logger.warning(f"Unsloth: skipped an optional source edit{detail} (anchor not found).")
return function
return function.replace(old, new, count)
# Fix tokenizer double BOS
def sft_trainer_prepare_dataset(function_name, function):
if function_name != "_prepare_non_packed_dataloader" and function_name != "_prepare_dataset":
return function
fast_sft_prepare_dataset = RL_REPLACEMENTS.get("sft_prepare_dataset", None)
if fast_sft_prepare_dataset is not None:
params = inspect.signature(fast_sft_prepare_dataset).parameters.keys()
params = ".*?".join(params)
matched = re.match(
r"[\s]{0,}def _prepare_dataset\(.*?" + params + r".*?\)",
function,
flags = re.MULTILINE | re.DOTALL,
)
if matched:
# Use fast version!
function = inspect.getsource(fast_sft_prepare_dataset)
# why: anchor the wrapped-packing setup on the function signature -- a
# structural anchor that always exists -- not the unsloth_zoo license comment,
# which is only lower-bounded and a newer Zoo may move or drop. Anchoring there
# let the setup silently no-op while edits below referenced its variables,
# NameError-ing every SFT dataset prep. Fail loudly if the signature is missing.
function, _n_setup = re.subn(
r"(def sft_prepare_dataset\s*\(.*?\)\s*(?:->[^:\n]*)?:[ \t]*\n)",
lambda match: match.group(1) + _WRAPPED_PACKING_SETUP,
function,
count = 1,
flags = re.DOTALL,
)
if _n_setup != 1:
raise RuntimeError(
"Unsloth: failed to install wrapped-packing support into "
"sft_prepare_dataset (signature not found); please file a bug report."
)
# why: route each edit through _require_replace so a drifted anchor fails
# loudly instead of leaving a dangling reference to the setup variables.
function = _require_replace(
function,
"truncation = do_truncation,",
"truncation = do_truncation and not _unsloth_wrapped_packing,",
where = "sft_prepare_dataset truncation flag",
)
function = _require_replace(
function,
"if do_truncation and max_seq_length > 0:",
"if do_truncation and not _unsloth_wrapped_packing and max_seq_length > 0:",
where = "sft_prepare_dataset truncation guard",
)
# why: reuse the guarded _unsloth_pack_has_strategy from the setup instead of
# re-calling _inspect.signature(pack_dataset) here -- the setup wraps that call
# in try/except, so a non-introspectable pack_dataset must not crash here.
function = _require_replace(
function,
"""dataset = pack_dataset(
dataset.select_columns(used_column_names),
max_seq_length,
getattr(args, "packing_strategy", "bfd"),
map_kwargs,
)""",
"""_pack_kwargs = {"map_kwargs": map_kwargs}
if _unsloth_pack_has_strategy:
_pack_kwargs["strategy"] = getattr(args, "packing_strategy", "bfd")
dataset = pack_dataset(
dataset.select_columns(used_column_names),
max_seq_length,
**_pack_kwargs,
)""",
where = "sft_prepare_dataset pack_dataset call",
)
function = function.split("\n")
function = "\n".join(" " * 4 + x for x in function)
function = function.replace("def sft_prepare_dataset", "def _prepare_dataset")
return function
check_text = (
"if 'skip_prepare_dataset' in locals() and skip_prepare_dataset:\n"
" return dataset\n"
"if 'tokenizer' not in locals(): tokenizer = processing_class\n"
"if 'formatting_func' not in locals(): raise RuntimeError('Unsloth: Please file a bug report - `formatting_func` does not exist!')\n"
"if 'dataset_text_field' not in locals() and 'args' in locals(): dataset_text_field = args.dataset_text_field\n"
"if 'dataset_text_field' not in locals(): dataset_text_field = None\n"
"if formatting_func is None and dataset_text_field is None and 'prompt' in dataset[0] and 'completion' in dataset[0]:\n"
" test_text = (dataset[0]['prompt'] + dataset[0]['completion']) if (isinstance(dataset[0]['prompt'], str) and isinstance(dataset[0]['completion'], str)) else None\n"
"elif formatting_func is None and dataset_text_field is not None:\n"
" test_text = dataset[0][dataset_text_field]\n"
"elif formatting_func is not None:\n"
" test_text = formatting_func(dataset[0])[0]\n"
"else:\n"
" test_text = None\n"
"chat_template = getattr(tokenizer, 'chat_template', None)\n"
"chat_template = '' if chat_template is None else chat_template\n"
"has_bos_token_already = ((test_text is not None and test_text.startswith(tokenizer.bos_token)) or tokenizer.bos_token in chat_template) "
"if getattr(tokenizer, 'bos_token', None) is not None else False\n"
"if 'add_special_tokens' not in locals() and has_bos_token_already:\n"
" from functools import partial\n"
" tokenizer_call = tokenizer.__call__\n"
" tokenizer.__call__ = partial(tokenizer_call, add_special_tokens = False)\n"
" processing_class = tokenizer\n"
"else:\n"
" tokenizer_call = None\n"
" add_special_tokens = False if has_bos_token_already else locals().get('add_special_tokens', False)\n"
)
check_text = check_text.split("\n")
check_text = "\n".join(" " * 8 + x for x in check_text)
check_text = check_text.rstrip() + "\n"
# .*? matches first match. .+? matches final match.
replacer = re.findall(
r"def " + function_name + r"\(.*?\).*?\:\n",
function,
flags = re.MULTILINE | re.DOTALL,
)
if len(replacer) != 0:
replacer = replacer[0]
function = function.replace(replacer, replacer + check_text)
# Return tokenizer's original state
return_state = "if tokenizer_call is not None: tokenizer.__call__ = tokenizer_call\n"
function = re.sub(
r"\n([ ]{4,})(return .*?[\s]{0,})$",
rf"\1{return_state}\1\2",
function,
)
return function
RL_FUNCTIONS["sft_trainer"].append(sft_trainer_prepare_dataset)
# Ignore mean_token_accuracy since it needs logits
# We override it directly with our version
def sft_trainer_compute_loss(function_name, function):
if function_name != "compute_loss":
return function
def compute_loss(
self,
model,
inputs,
return_outputs = False,
num_items_in_batch = None,
):
outputs = super().compute_loss(
model,
inputs,
return_outputs = return_outputs,
num_items_in_batch = num_items_in_batch,
)
return outputs
function = inspect.getsource(compute_loss)
return function
RL_FUNCTIONS["sft_trainer"].append(sft_trainer_compute_loss)
# Route ORPO/CPO row tokenization through the underlying text tokenizer when the
# processing class is a multimodal processor; CPO reuses this code (#4952).
def orpo_trainer_text_tokenizer(function_name, function):
if function_name == "build_tokenized_answer":
function = re.sub(
r"(?m)^([ \t]*)full_tokenized = self\.processing_class\(prompt \+ answer, add_special_tokens=False\)\n"
r'\1prompt_input_ids = self\.processing_class\(prompt, add_special_tokens=False\)\["input_ids"\]\n',
r'\1tokenizer = getattr(self.processing_class, "tokenizer", self.processing_class)'
"\n"
r"\1full_tokenized = tokenizer(prompt + answer, add_special_tokens=False)"
"\n"
r'\1prompt_input_ids = tokenizer(prompt, add_special_tokens=False)["input_ids"]'
"\n",
function,
count = 1,
)
return function
if function_name != "tokenize_row":
return function
if (
'tokenizer = getattr(self.processing_class, "tokenizer", self.processing_class)'
not in function
):
new_function = re.sub(
r"(?m)^([ \t]*)batch = \{\}\n",
r"\1batch = {}"
"\n"
r'\1tokenizer = getattr(self.processing_class, "tokenizer", self.processing_class)'
"\n",
function,
count = 1,
)
if new_function == function:
return function
function = new_function
function = function.replace("self.processing_class(", "tokenizer(")
function = function.replace("self.processing_class.bos_token_id", "tokenizer.bos_token_id")
function = function.replace("self.processing_class.eos_token_id", "tokenizer.eos_token_id")
return function
RL_FUNCTIONS["orpo_trainer"].append(orpo_trainer_text_tokenizer)
RL_FUNCTIONS["cpo_trainer"].append(orpo_trainer_text_tokenizer)
# Resolve `processing_class.pad_token_id` through the underlying tokenizer when
# a multimodal processor is supplied (processors lack `pad_token_id`). Without
# this, ORPO/CPOTrainer.__init__ raises AttributeError on
# `DPODataCollatorWithPadding(pad_token_id=processing_class.pad_token_id, ...)`
# and on `self.padding_value = ... else processing_class.pad_token_id`.
_PAD_FALLBACK = (
"(getattr(processing_class, 'pad_token_id', None) "
"if getattr(processing_class, 'pad_token_id', None) is not None "
"else getattr(getattr(processing_class, 'tokenizer', None), 'pad_token_id', None))"
)
def orpo_trainer_processor_pad_token(function_name, function):
if function_name != "__init__":
return function
# Multimodal processors (e.g. Gemma3/Gemma4 Processor) expose pad_token /
# eos_token on `.tokenizer`, not on the processor itself. TRL 1.x CPO/ORPO
# __init__ defaults `processing_class.pad_token` from `.eos_token` before
# tokenizing, which AttributeErrors on such a processor. Route the default
# through the inner tokenizer. Older TRL lacks this block, so the sub is a
# no-op there and only the pad_token_id fallback below applies.
function = re.sub(
r"(?m)^([ \t]*)if processing_class\.pad_token is None:\n"
r"\1[ \t]+processing_class\.pad_token\s*=\s*processing_class\.eos_token\n",
r"\1_unsloth_proc_tok = getattr(processing_class, 'tokenizer', processing_class)\n"
r"\1if getattr(_unsloth_proc_tok, 'pad_token', None) is None:\n"
r"\1 _unsloth_proc_tok.pad_token = getattr(_unsloth_proc_tok, 'eos_token', None)\n",
function,
count = 1,
)
if "processing_class.pad_token_id" not in function:
return function
return function.replace("processing_class.pad_token_id", _PAD_FALLBACK)
RL_FUNCTIONS["orpo_trainer"].append(orpo_trainer_processor_pad_token)
RL_FUNCTIONS["cpo_trainer"].append(orpo_trainer_processor_pad_token)
# Fix bare pop("push_to_hub_token") in compiled SFT/IterativeSFT trainer __init__
# On transformers 5.0+, to_dict() no longer includes push_to_hub_token, so bare pop KeyErrors
def sft_trainer_push_to_hub_token(function_name, function):
if function_name != "__init__":
return function
return function.replace(
'dict_args.pop("push_to_hub_token")', 'dict_args.pop("push_to_hub_token", None)'
)
RL_FUNCTIONS["sft_trainer"].append(sft_trainer_push_to_hub_token)
# Autocast precision for GRPO
def grpo_trainer__prepare_inputs(function_name, function):
if function_name != "_prepare_inputs":
return function
# Add mixed precision training
function = function.replace(
"with torch.inference_mode():",
"with torch.inference_mode(), "
"torch.amp.autocast(device_type = 'cuda', "
"dtype = ((torch.float16 if os.environ.get('ACCELERATE_MIXED_PRECISION', 'fp16') == 'fp16' else torch.bfloat16) "
"if not torch.is_autocast_enabled('cuda') else nullcontext())"
"if os.environ.get('UNSLOTH_FORCE_FLOAT32', '0') == '0' else torch.float16):",
)
function = function.replace(
"self.accelerator.unwrap_model(self.model)",
"self.accelerator.unwrap_model(self.model, keep_fp32_wrapper = False)",
)
return function
RL_FUNCTIONS["grpo_trainer"].append(grpo_trainer__prepare_inputs)
# Guard reload_weights and sync_weights - skip when fast inference LoRA shares weights with vLLM
# https://github.com/huggingface/trl/commit/7856d3b1f6518601732f489883b341bb6dd36434#diff-964e6fd373aa93037604064cb2b822d7f8e2735e33f791065acf2c4c3552d393R1168-R1169
def _guard_vllm_sync_reload_for_shared_weights(function):
# Guard reload_weights - only call when not sharing weights with vLLM
reload_weights_pattern = re.compile(
r"^(?P<indent>[ \t]*)self\.llm\.collective_rpc\(\s*(['\"])reload_weights\2\s*\)\s*$",
re.MULTILINE,
)
def replace_reload_weights_line(match):
indent = match.group("indent")
return (
f"{indent}if not getattr(self.llm, 'shared_weights', False):\n"
f'{indent} self.llm.collective_rpc("reload_weights")\n'
)
function = reload_weights_pattern.sub(replace_reload_weights_line, function)
# Guard sync_weights - skip when sharing weights with vLLM
sync_weights_block = re.compile(
r"(?P<indent>[ \t]*)with profiling_context\(self,\s*(['\"])sync_weights\2\s*\):\n"
r"(?P=indent)[ \t]+self\.vllm_generation\.sync_weights\(\)\n",
re.MULTILINE,
)
def guard_sync_weights_block(match):
indent = match.group("indent")
return (
f"{indent}if not getattr(getattr(self.vllm_generation, 'llm', None), 'shared_weights', False):\n"
f"{indent} with profiling_context(self, 'sync_weights'):\n"
f"{indent} self.vllm_generation.sync_weights()\n"
)
function = sync_weights_block.sub(guard_sync_weights_block, function)
return function
def grpo_trainer__generate_single_turn(function_name, function):
if function_name != "_generate_single_turn":
return function
function = _guard_vllm_sync_reload_for_shared_weights(function)
# TRL 0.24.0-0.25.1 truncation regression fix
#
# TRL 0.22.2-0.23.1 used smart truncation via truncate_with_protected_tokens():
# - Tokenizes first without truncation
# - Then truncates keeping the RIGHTMOST tokens (preserves assistant turn)
# - Protects special tokens (image_token, vision_start/end) from removal
#
# TRL 0.24.0-0.25.1 removed this and passed kwargs directly to the tokenizer:
# max_length=self.max_prompt_length, truncation=True, add_special_tokens=False
# This causes issues because tokenizer truncation doesn't protect special tokens
# and may not preserve the end of the prompt properly.
#
# TRL 0.26.2+ removed these kwargs entirely (no tokenizer-level truncation).
#
# Fix: Remove these kwargs so TRL 0.24.0-0.25.1 behaves like 0.26.2+ (no truncation).
# This is a no-op for versions that don't have these kwargs (0.22.2-0.23.1, 0.26.2+).
for pattern in [
r'["\']?max_length["\']?\s*[:=]\s*self\.max_prompt_length\s*,\s*\n?',
r'["\']?truncation["\']?\s*[:=]\s*True\s*,\s*\n?',
r'["\']?add_special_tokens["\']?\s*[:=]\s*False\s*,\s*\n?',
]:
function = re.sub(pattern, "", function)
string_to_find = " generate_inputs = super()._prepare_inputs(generate_inputs)"
replacement_string = (
string_to_find
+ """
if "mm_token_type_ids" in generate_inputs or "image_grid_thw" in generate_inputs:
mm_token_type_ids = _unsloth_fix_mm_token_type_ids(
self.processing_class,
generate_inputs["input_ids"],
generate_inputs.get("mm_token_type_ids", None),
)
if mm_token_type_ids is not None:
generate_inputs["mm_token_type_ids"] = mm_token_type_ids"""
)
function = function.replace(string_to_find, replacement_string)
return function
RL_FUNCTIONS["grpo_trainer"].append(grpo_trainer__generate_single_turn)
def grpo_trainer__generate(function_name, function):
if function_name != "_generate":
return function
return _guard_vllm_sync_reload_for_shared_weights(function)
RL_FUNCTIONS["grpo_trainer"].append(grpo_trainer__generate)
# Fix incorrect special tokens handling and truncation in older TRL versions
def grpo_trainer__generate_and_score_completions(function_name, function):
if function_name != "_generate_and_score_completions":
return function
# TRL 0.19.0 did skip_special_tokens = True which should be False
function = function.replace(
"prompt_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False",
"prompt_ids, skip_special_tokens=False, clean_up_tokenization_spaces=False",
)
# Left pad prompt before calculation old and ref hidden states
line_to_replace = 'batch_size = self.args.per_device_train_batch_size if mode == "train" else self.args.per_device_eval_batch_size'
# The new multi-line string that will replace the line above
replacement_lines = """
max_left_pad = None
batch_size = self.args.per_device_train_batch_size if mode == "train" else self.args.per_device_eval_batch_size
try:
# TRL 0.23.1 and below path
if not has_images:
# Left pad prompt before calculation old and ref hidden states
left_pad_tokens_per_prompt = calculate_pad_tokens_in_prompt(prompt_completion_ids, logits_to_keep, self.processing_class.pad_token_id)
max_left_pad = torch.max(left_pad_tokens_per_prompt).item()
except:
# TRL 0.24.0 and below path
if images is None:
# Left pad prompt before calculation old and ref hidden states
left_pad_tokens_per_prompt = calculate_pad_tokens_in_prompt(prompt_completion_ids, logits_to_keep, self.processing_class.pad_token_id)
max_left_pad = torch.max(left_pad_tokens_per_prompt).item()
_use_gc = self.model._unsloth_gradient_checkpointing if hasattr(self.model, '_unsloth_gradient_checkpointing') else getattr(self.args, 'gradient_checkpointing', True)
self.model.for_training(use_gradient_checkpointing=_use_gc)"""
function = function.replace(line_to_replace, replacement_lines)
pattern_to_find = re.compile(
r"^\s*if self\.args\.gradient_accumulation_steps % generate_every != 0 or \(\s*"
r"self\.use_vllm and self\.vllm_importance_sampling_correction\s*"
r"\):",
re.MULTILINE,
)
replacement_text = """
if self.args.gradient_accumulation_steps % generate_every != 0 or (
self.use_vllm
):"""
# Use re.sub() to perform the replacement
function, num_replacements = pattern_to_find.subn(replacement_text, function)
pattern_to_find = re.compile(
r"(^\s*)all_logprobs = \[" # Capture indentation (group 1)
r".*?" # Match everything inside non-greedily
r"for output in outputs\.outputs\s*"
r"\]",
re.DOTALL | re.MULTILINE,
)
# sanitize_logprob is injected as a module-level function via RLTrainer_replacement
# template in rl.py (from RL_REPLACEMENTS), so just reference it directly here.
replacement_text = (
r"\1all_logprobs = [\n"
r"\1 [sanitize_logprob(next(iter(logprob.values()))) for logprob in output.logprobs]\n"
r"\1 for outputs in all_outputs\n"
r"\1 for output in outputs.outputs\n"
r"\1]"
)
function, num_replacements = pattern_to_find.subn(replacement_text, function)
# Always between max_prompt_length and use_vllm
found = re.findall(
r"\n(([ ]{8,})if self\.max_prompt_length is not None:.*?\2if self\.use_vllm:)",
function,
flags = re.DOTALL | re.MULTILINE,
)
if len(found) != 0:
replace_part, spacing = found[0]
removed_comments = re.sub(r"\#[^\n]{1,}", "", replace_part)
splits = removed_comments.split("\n")
if (
sum(re.match(rf"{spacing}[^\s]", x) is not None for x in splits) == 2
and len(spacing) >= 8
):
new_replacement = f"""\n{spacing}if self.max_prompt_length is not None:
# If max_prompt_length is set, we trim the prompt to keep only the last `max_prompt_length` tokens.
# Then we decode those tokens back into text. We manually remove leading pad tokens from the decoded text,
# because we can't use `skip_special_tokens=True` (some special tokens are still needed for generation).
protected = [self.image_token_id, self.vision_start_token_id, self.vision_end_token_id]
protected = [token for token in protected if token is not None]
prompt_ids, prompt_mask = truncate_with_protected_tokens(
prompt_ids, prompt_mask, self.max_prompt_length, protected
)
prompts_text = [re.sub(rf"^({{re.escape(self.pad_token)}})+", "", text) for text in prompts_text]
# The chat template inserts a single image token into the prompt text. However, when this text is later
# tokenized, the single image token string is expanded into multiple image token IDs, depending on the
# image size. Since we're detokenizing here, we may see repeated image tokens in the decoded text. We
# collapse them back into a single token string to match the original template.
if self.image_token is not None:
prompts_text = [
re.sub(rf"({{re.escape(self.image_token)}})+", self.image_token, text) for text in prompts_text
]
# Generate completions using either vLLM or regular generation
if self.use_vllm:"""
function = function.replace(replace_part, new_replacement)
# Important note: we disable TRL's importance sampling logic
# It is disabled because the LLM path moves left padding to the right.
# We must adjust the vLLM sampling_logprob tensor in Unsloth to account for this.
string_to_find = "if self.use_vllm and self.vllm_importance_sampling_correction:"
replacement_string = "if False and self.use_vllm and self.vllm_importance_sampling_correction:"
function = function.replace(string_to_find, replacement_string)
string_to_find = """ if "image_sizes" in prompt_inputs:
output["image_sizes"] = prompt_inputs["image_sizes"]"""
replacement_string = """ if "image_sizes" in prompt_inputs:
output["image_sizes"] = prompt_inputs["image_sizes"]
if max_left_pad is not None:
output["max_left_pad"] = torch.tensor(prompt_ids.shape[0] * [max_left_pad]).unsqueeze(-1)
try:
if self.use_vllm and getattr(self, "vllm_importance_sampling_correction", False):
output["sampling_per_token_logps"] = sampling_per_token_logps
except NameError:
output["sampling_per_token_logps"] = None"""
function = function.replace(string_to_find, replacement_string)
# TRL 0.24.0+ extracts prompts = [x["prompt"] for x in inputs], losing metadata
# like reasoning_effort. Inject code to store per-sample chat_template_kwargs on self.
_metadata_extraction = (
"\n"
" # Unsloth: Extract per-sample chat_template_kwargs before metadata is lost\n"
" _ct_ = getattr(self.processing_class, 'chat_template', None) or ''\n"
" _sk_ = {'prompt', 'chosen', 'rejected', 'completion', 'messages', 'label',\n"
" 'images', 'image', 'videos', 'video', 'audios', 'audio'}\n"
" self._unsloth_batch_chat_kwargs = []\n"
" for _inp_ in inputs:\n"
" _kw_ = {}\n"
" if isinstance(_inp_, dict):\n"
" for _k_ in _inp_.keys() - _sk_:\n"
" if _k_ in _ct_ and isinstance(_inp_[_k_], str):\n"
" _kw_[_k_] = _inp_[_k_]\n"
" self._unsloth_batch_chat_kwargs.append(_kw_)\n"
)
# Insert after: prompts = [x["prompt"] for x in inputs]
_target_line = 'prompts = [x["prompt"] for x in inputs]'
if _target_line in function:
function = function.replace(
_target_line,
_target_line + _metadata_extraction,
)
# This path is for TRL 0.24.0 images is a variable exclusive to this version
string_to_find = """ if images is not None:
output["num_images"] = num_images"""
replacement_string = """ if images is not None:
output["num_images"] = num_images
if max_left_pad is not None:
output["max_left_pad"] = torch.tensor(prompt_ids.shape[0] * [max_left_pad]).unsqueeze(-1)
try:
if self.use_vllm and getattr(self, "vllm_importance_sampling_correction", False):
output["sampling_per_token_logps"] = sampling_per_token_logps
except NameError:
output["sampling_per_token_logps"] = None"""
function = function.replace(string_to_find, replacement_string)
if trl_version >= Version("0.24.0"):
# We replace the call using 'completions' with one using 'completions_text'
string_to_find = " rewards_per_func = self._calculate_rewards(inputs, prompts, completions, completion_ids_list)"
replacement_string = (
" if images is not None:\n"
" rewards_per_func = self._calculate_rewards(inputs, prompts_text, completions_text, completion_ids_list)\n"
" else:\n"
" rewards_per_func = self._calculate_rewards(inputs, prompts, completions, completion_ids_list)"
)
function = function.replace(string_to_find, replacement_string)
_generate_return = """ ) = self._generate(prompts)"""
if _generate_return in function and "_unsloth_clear_stateful_mrope" not in function:
function = function.replace(
_generate_return,
_generate_return
+ """
_unsloth_clear_stateful_mrope(
self.accelerator.unwrap_model(self.model, keep_fp32_wrapper = False)
)""",
)
if "wake_up()" not in function:
# Sleep functionality has been added to trl in v0.23.0. We do not want to redo this.
# https://github.com/huggingface/trl/commit/edbe8234bc7e528f72ac76607de9d3e4753e2709
pattern = re.compile(r".*self\.llm\.generate\(.*\).*", re.MULTILINE)
matches = list(pattern.finditer(function))
patched = function
# Generally there's only one match. But this is just to make sure we don't miss any.
for match in reversed(matches):
line = match.group(0)
indent_match = re.match(r"(\s*)", line)
indent = indent_match.group(1) if indent_match else ""
wrapped = (
f"{indent}if hasattr(self, 'llm'):\n"
f"{indent} if getattr(self.llm.llm_engine.vllm_config.model_config, 'enable_sleep_mode', False):\n"
f"{indent} self.llm.wake_up()\n"
f"{line}\n\n"
f"{indent}if hasattr(self, 'llm'):\n"
f"{indent} if getattr(self.llm.llm_engine.vllm_config.model_config, 'enable_sleep_mode', False):\n"
f"{indent} self.llm.sleep(os.environ.get('VLLM_SLEEP_MODE', 1))\n"
)
patched = patched[: match.start()] + wrapped + patched[match.end() :]
function = patched
_mm_alignment = """
if "mm_token_type_ids" in forward_kwargs or "image_grid_thw" in forward_kwargs:
_mm_token_type_ids = _unsloth_fix_mm_token_type_ids(
self.processing_class,
prompt_completion_ids,
forward_kwargs.get("mm_token_type_ids", None),
completion_ids = completion_ids,
)
if _mm_token_type_ids is not None:
forward_kwargs["mm_token_type_ids"] = _mm_token_type_ids
"""
_tool_image_marker = (
" # For VLM tool images: build token type IDs from the full prompt_completion_ids."
)
if _tool_image_marker in function:
function = function.replace(_tool_image_marker, _mm_alignment + "\n" + _tool_image_marker)
else:
_tt_search = (
'if "token_type_ids" in forward_kwargs:\n'
' token_type_ids = forward_kwargs["token_type_ids"]\n'
' forward_kwargs["token_type_ids"] = torch.cat(\n'
" [token_type_ids, token_type_ids.new_zeros(completion_ids.shape)], dim=1\n"
" )"
)
function = function.replace(_tt_search, _tt_search + "\n" + _mm_alignment.rstrip())
_save_search = (
'if "token_type_ids" in forward_kwargs:\n'
' output["token_type_ids"] = forward_kwargs["token_type_ids"]'
)
if 'output["mm_token_type_ids"]' not in function:
_save_replace = (
_save_search + "\n"
' if "mm_token_type_ids" in forward_kwargs:\n'
' output["mm_token_type_ids"] = forward_kwargs["mm_token_type_ids"]'
)
function = function.replace(_save_search, _save_replace)
if re.search(r"\btool_mask\b", function) and 'output["tool_mask"]' not in function:
function = function.replace(
" return output",
" if tool_mask is not None:\n"
' output["tool_mask"] = tool_mask\n'
" return output",
)
return function
RL_FUNCTIONS["grpo_trainer"].append(grpo_trainer__generate_and_score_completions)
# Fix {"reasoning_effort" : "high"} not applied
def grpo_trainer_fix_maybe_apply_chat_template(function_name, function):
spaces = function.find("def ")
if spaces % 4 != 0:
return function
spaces += 4
replacement = """
_chat_template_ = getattr(self.processing_class, "chat_template", None)
if _chat_template_ is None: _chat_template_ = ""
_supported_keys_ = set(("prompt", "chosen", "rejected", "completion", "messages", "label"))
_batch_chat_kwargs_ = getattr(self, "_unsloth_batch_chat_kwargs", None)
prompts_text = []
for _idx_, _example_ in enumerate(__INPUTS__REPLACEMENT__):
_tokenizer_kwargs_ = {}
if type(_example_) is not dict:
_example_ = {"prompt": _example_}
_left_keys_ = _example_.keys() - _supported_keys_
for k in _left_keys_:
if k in _chat_template_:
v = _example_[k]
if type(v) is str:
_tokenizer_kwargs_[k] = v
if _batch_chat_kwargs_ is not None and _idx_ < len(_batch_chat_kwargs_):
for _bk_, _bv_ in _batch_chat_kwargs_[_idx_].items():
if _bk_ not in _tokenizer_kwargs_:
_tokenizer_kwargs_[_bk_] = _bv_
_x_ = maybe_apply_chat_template(_example_, self.processing_class, **_tokenizer_kwargs_)["prompt"]
prompts_text.append(_x_)
"""
replacement = textwrap.dedent(replacement).strip()
replacement = textwrap.indent(replacement, spaces * " ")
replacement = f"\n{replacement}\n"
what = 'prompts_text = [maybe_apply_chat_template(example, self.processing_class)["prompt"] for example in inputs]'
function = function.replace(what, replacement.replace("__INPUTS__REPLACEMENT__", "inputs"))
"""prompts_text = [
maybe_apply_chat_template({"prompt": prompt}, self.processing_class)["prompt"] for prompt in prompts
]"""
function = re.sub(
r"prompts_text = \["
r"[\s]{0,}"
r"maybe_apply_chat_template\(\{[\"\']prompt[\"\'][\s]{0,}\:[\s]{0,}prompt[\s]{0,}\}[\s]{0,}\,[\s]{0,}self\.processing_class\)"
r"\[[\"\']prompt[\"\']\] for prompt in prompts"
r"[\s]{0,}"
r"\]",
replacement.replace("__INPUTS__REPLACEMENT__", "prompts"),
function,
)
return function
RL_FUNCTIONS["grpo_trainer"].append(grpo_trainer_fix_maybe_apply_chat_template)
# Remove _move_model_to_vllm
def grpo_trainer__move_model_to_vllm(function_name, function):
if function_name != "_move_model_to_vllm":
return function
def _move_model_to_vllm(self, *args, **kwargs):
return None
function = inspect.getsource(_move_model_to_vllm)
return function
RL_FUNCTIONS["grpo_trainer"].append(grpo_trainer__move_model_to_vllm)
# Edit _get_per_token_logps to handle mixed precision
def grpo_trainer__get_per_token_logps(function_name, function):
if function_name != "_get_per_token_logps":
return function
def _get_per_token_logps(
self,
model,
input_ids,
attention_mask,
logits_to_keep,
compute_efficient = False,
):
if True: # os.environ.get('UNSLOTH_USE_NEW_MODEL', '0') == '0':
return None # Unsloth efficient GRPO
# Otherwise, calculate normally:
if not hasattr(self, "_autocast_dtype"):
self._autocast_dtype = (
torch.float16
if os.environ.get("ACCELERATE_MIXED_PRECISION", "fp16") == "fp16"
else torch.bfloat16
)
if os.environ.get("UNSLOTH_FORCE_FLOAT32", "0") == "1":
self._autocast_dtype = torch.float16
os.environ["UNSLOTH_RETURN_HIDDEN_STATES"] = "1"
with torch.amp.autocast(device_type = DEVICE_TYPE, dtype = self._autocast_dtype):
# We add 1 to `logits_to_keep` because the last logits of the sequence is later excluded
logits = model(
input_ids = input_ids,
attention_mask = attention_mask,
logits_to_keep = logits_to_keep + 1,
).logits
# logits = logits[:, :-1, :] # (B, L-1, V), exclude the last logit: it corresponds to the next token pred
return logits
# input_ids = input_ids[:, -logits_to_keep:]
# For transformers<=4.48, logits_to_keep argument isn't supported, so here we drop logits ourselves.
# See https://github.com/huggingface/trl/issues/2770
# logits = logits[:, -logits_to_keep:]
# return logits
# See https://huggingface.co/blog/the_n_implementation_details_of_rlhf_with_ppo#policy-training-implementation-details
# logits = logits / self.temperature
# logps = selective_log_softmax(logits, input_ids)
# row_indices, col_indices = torch.where(logps < -20)
# # Method 1: Check if tensors have elements
# if len(row_indices) > 0 and len(col_indices) > 0:
# breakpoint() # Breakpoint triggered here
# print("Found high values!")
# return logps # compute logprobs for the input tokens
function = inspect.getsource(_get_per_token_logps)
return function
RL_FUNCTIONS["grpo_trainer"].append(grpo_trainer__get_per_token_logps)
def grpo_trainer__get_per_token_logps_and_entropies(function_name, function):
if function_name != "_get_per_token_logps_and_entropies":
return function
# Just copy over from _get_per_token_logps replacement function above. For now this returns None anyway
def _get_per_token_logps_and_entropies(
self,
model,
input_ids,
attention_mask,
logits_to_keep,
batch_size = None,
compute_entropy = False,
compute_efficient = False,
*args,
**kwargs,
):
# All Unsloth code here in this function is licensed under AGPL3
# if True: # os.environ.get('UNSLOTH_USE_NEW_MODEL', '0') == '0':
# return None, None # logps, entropies Unsloth efficient GRPO
if compute_efficient:
return None, None
else:
if not hasattr(self, "_autocast_dtype"):
self._autocast_dtype = (
torch.float16
if os.environ.get("ACCELERATE_MIXED_PRECISION", "fp16") == "fp16"
else torch.bfloat16
)
if os.environ.get("UNSLOTH_FORCE_FLOAT32", "0") == "1":
self._autocast_dtype = torch.float16
compute_aux_loss = kwargs.get("compute_aux_loss", None)
pixel_values, image_grid_thw = (
kwargs.get("pixel_values", None),
kwargs.get("image_grid_thw", None),
)
pixel_attention_mask, image_sizes = (
kwargs.get("pixel_attention_mask", None),
kwargs.get("image_sizes", None),
)
num_images = kwargs.get("num_images", None)
# Transformers 5.x needs token_type_ids/mm_token_type_ids for some vision models
token_type_ids = kwargs.get("token_type_ids", None)
mm_token_type_ids = kwargs.get("mm_token_type_ids", None)
if mm_token_type_ids is not None or image_grid_thw is not None:
mm_token_type_ids = _unsloth_fix_mm_token_type_ids(
self.processing_class, input_ids, mm_token_type_ids
)
unwrapped_model = self.accelerator.unwrap_model(model, keep_fp32_wrapper = False)
lm_head = self.model.get_output_embeddings().weight
dtype_bytes = 16 if self._autocast_dtype in [torch.float16, torch.bfloat16] else 32
total_rows = input_ids.shape[0]
seq_len = input_ids.shape[1]
hidden_dim = lm_head.shape[1]
vocab_dim = lm_head.shape[0]
if self.args.unsloth_grpo_mini_batch is None:
B, multiplier = autotune_batch_and_chunks(
total_rows,
seq_len,
hidden_dim,
vocab_dim,
dtype_bytes,
self.args.unsloth_logit_chunk_multiplier,
)
B = total_rows // B
else:
B = self.args.unsloth_grpo_mini_batch
if self.args.unsloth_logit_chunk_multiplier is None:
multiplier = max(4, seq_len // 4096)
else:
multiplier = self.args.unsloth_logit_chunk_multiplier
all_logprobs_list = []
if pixel_values is None:
left_pad_tokens_per_prompt = calculate_pad_tokens_in_prompt(
input_ids, logits_to_keep, self.processing_class.pad_token_id
)
max_left_pad = torch.max(left_pad_tokens_per_prompt).item()
input_ids = left_pack_padding(input_ids, self.processing_class.pad_token_id)
attention_mask = input_ids != self.processing_class.pad_token_id
attention_mask = attention_mask.to(attention_mask.dtype)
else:
max_left_pad = 0
def slice_sample_axis(value, start, end):
if value is None:
return None
return value[start:end]
import math
total_samples = input_ids.shape[0]
batch_size = math.ceil(total_samples / B)
if isinstance(num_images, torch.Tensor):
num_images = num_images.detach().cpu().reshape(-1).tolist()
if image_grid_thw is not None and pixel_values is not None and num_images is not None:
rows_per_image = image_grid_thw.prod(dim = -1)
rows_per_sample = torch.split(rows_per_image, num_images)
rows_per_sample = torch.stack([s.sum() for s in rows_per_sample])
# why: cum_rows is indexed via .item() inside the per-chunk loop;
# keeping it on CPU avoids per-iteration GPU->CPU sync.
cum_rows = torch.cat(
[
torch.tensor([0], device = rows_per_sample.device),
rows_per_sample.cumsum(0),
]
).cpu()
cum_imgs = torch.tensor([0] + num_images).cumsum(0)
else:
cum_rows = None
cum_imgs = None
def _first_dim_len(value):
if value is None:
return None
if hasattr(value, "shape"):
return value.shape[0]
try:
return len(value)
except TypeError:
return None
total_images = sum(num_images) if num_images is not None else None
_image_sizes_n = _first_dim_len(image_sizes)
input_ids_chunks = []
attention_mask_chunks = []
pixel_values_chunks = []
image_grid_thw_chunks = []
pixel_attention_mask_chunks = []
image_sizes_chunks = []
token_type_ids_chunks = []
mm_token_type_ids_chunks = []
current_pixel_idx = 0
# TRL 0.23.0 batching logic
for start in range(0, total_samples, batch_size):
end = min(start + batch_size, total_samples)
input_ids_chunks.append(input_ids[start:end])
attention_mask_chunks.append(attention_mask[start:end])
token_type_ids_chunks.append(slice_sample_axis(token_type_ids, start, end))
mm_token_type_ids_chunks.append(slice_sample_axis(mm_token_type_ids, start, end))
if image_grid_thw is not None and pixel_values is not None:
if num_images is None:
grid_slice = image_grid_thw[start:end]
batch_pixel_count = grid_slice.prod(dim = -1).sum().item()
start_pixel_idx = current_pixel_idx
end_pixel_idx = current_pixel_idx + batch_pixel_count
current_pixel_idx = end_pixel_idx
img_start = img_end = None
else:
start_pixel_idx = cum_rows[start].item()
end_pixel_idx = cum_rows[end].item()
img_start = cum_imgs[start].item()
img_end = cum_imgs[end].item()
grid_slice = image_grid_thw[img_start:img_end]
image_grid_thw_chunks.append(grid_slice)
pixel_values_chunks.append(pixel_values[start_pixel_idx:end_pixel_idx])
if image_sizes is None:
image_sizes_chunks.append(None)
elif (
num_images is not None
and _image_sizes_n == total_images
and img_start is not None
):
image_sizes_chunks.append(image_sizes[img_start:img_end])
else:
image_sizes_chunks.append(slice_sample_axis(image_sizes, start, end))
if pixel_attention_mask is None:
pixel_attention_mask_chunks.append(None)
elif (
num_images is not None
and img_start is not None
and pixel_attention_mask.shape[0] == image_grid_thw.shape[0]
):
pixel_attention_mask_chunks.append(pixel_attention_mask[img_start:img_end])
elif (
pixel_attention_mask.shape[0] == pixel_values.shape[0]
and pixel_attention_mask.shape[0] != input_ids.shape[0]
):
pixel_attention_mask_chunks.append(
pixel_attention_mask[start_pixel_idx:end_pixel_idx]
)
else:
pixel_attention_mask_chunks.append(pixel_attention_mask[start:end])
else:
pixel_values_chunks.append(None)
image_grid_thw_chunks.append(None)
pixel_attention_mask_chunks.append(None)
image_sizes_chunks.append(slice_sample_axis(image_sizes, start, end))
temperature = self.temperature
model_config = _unsloth_get_model_config(model)
logit_softcapping = _unsloth_get_final_logit_softcapping(model)
logit_scale_multiply = getattr(model_config, "logit_scale", 0)
if logit_scale_multiply is None:
logit_scale_multiply = 0
logit_scale_divide = getattr(model_config, "logits_scaling", 0)
if logit_scale_divide is None:
logit_scale_divide = 0
zipped_inputs = zip(
input_ids_chunks,
attention_mask_chunks,
pixel_values_chunks,
image_grid_thw_chunks,
pixel_attention_mask_chunks,
image_sizes_chunks,
token_type_ids_chunks,
mm_token_type_ids_chunks,
)
os.environ["UNSLOTH_RETURN_HIDDEN_STATES"] = "1"
# ---- Sequence packing (default-on; disable with UNSLOTH_GRPO_SEQ_PACKING=0) ----
# One varlen [1, sum L] forward replaces the padded [B, Lmax] loop (also fixes the
# left-pad RoPE error). Self-verified against the per-row forward, re-checked as T
# grows; falls back if a backend ignores packed_seq_lengths.
logprobs = None
# ---- PrefixGrouper (GRPO shared-prompt dedup; default ON, exact + self-verified) ----
# G completions per prompt share the prefix; the packed path forwards it G times,
# PrefixGrouper stores it once (FlexAttention shared-prefix mask), cutting the trunk
# forward from G*(P+R) to P+G*R tokens. Gated by UNSLOTH_GRPO_PREFIX_GROUPER (needs
# seq-packing), tok_r auto-gate, and first-use self-verify vs the packed path
# (mismatch => fall back + mark unsafe), so a mask/isolation regression cannot ship
# silently. When off / ungrouped / unverified, the packed path below runs as before.
_pg_result = None
_pg_use = False
_pg_skip_pk = False # once a shape is PG-verified, skip the full-row forward
_pg_forward_fn = None # deferred PG forward (runs at the verify site below)
_pg_num_gen = getattr(self, "num_generations", None)
# Env gate hoisted to module level (mirrored via RL_PRE_ITEMS). Skip PG under vLLM
# (fast_inference=True): the rollout dominates the step, so PG saves little and its
# first-use self-verify is net overhead.
_pg_engage = (
UNSLOTH_GRPO_PREFIX_GROUPER_ON
and not getattr(self, "use_vllm", False)
and not getattr(unwrapped_model, "_unsloth_prefix_grouper_nograd_disabled", False)
)
if _pg_engage:
try:
# Skip softcap models (the flex kernel never applies attn_logit_softcapping)
# and hybrid SSM / MoE models: only the threaded attention forwards get the
# shared-prefix isolation, so a Mamba or MoE decoder that does not forward
# prefix_seg_info would leak suffixes across completions. PG also rides on
# sequence packing, so it needs the same zoo masked-column guard.
_pg_cfg = getattr(unwrapped_model, "config", None)
_pg_engage = (
_pg_enabled_fn()
and UNSLOTH_ZOO_HAS_MASKED_COL_GUARD
and pixel_values is None
and token_type_ids is None
and mm_token_type_ids is None
and _pg_num_gen is not None
and _pg_num_gen >= 2
and not getattr(_pg_cfg, "attn_logit_softcapping", None)
# normal backends apply config.attention_dropout in training; the flex
# path is deterministic, so skip PG when it is set.
and not getattr(_pg_cfg, "attention_dropout", 0)
and not any(
getattr(_pg_cfg, _pg_a, None) is not None
for _pg_a in (
"mamba_d_ssm",
"mamba_d_state",
"mamba_expand",
"num_experts",
"num_local_experts",
"n_routed_experts",
"moe_intermediate_size",
)
)
)
except Exception:
_pg_engage = False
if _pg_engage:
try:
_pg_pad = self.processing_class.pad_token_id
# cap the PG span (P+max(R)) at the sliding window, like the packed _pk_sw guard.
_pg_sw = getattr(
getattr(unwrapped_model, "config", None), "sliding_window", None
)
if not (isinstance(_pg_sw, int) and _pg_sw > 0):
_pg_sw = None
_pg_layout = _pg_build_layout(
input_ids,
logits_to_keep,
_pg_pad,
_pg_num_gen,
left_pad_tokens_per_prompt,
max_segment_cap = _pg_sw,
)
_pg_unsafe = getattr(
unwrapped_model, "_unsloth_prefix_grouper_nograd_unsafe", None
)
if _pg_unsafe is None:
_pg_unsafe = set()
if _pg_layout is not None and _pg_layout.signature not in _pg_unsafe:
_pg_sig = _pg_layout.signature
_pg_verified = getattr(
unwrapped_model, "_unsloth_prefix_grouper_nograd_verified", None
)
if _pg_verified is None:
_pg_verified = set()
_pg_chunks = max(1, total_rows * multiplier)
def _pg_run_forward(_pg_layout = _pg_layout, _pg_chunks = _pg_chunks):
with _get_inference_mode_context_manager(model):
with torch.amp.autocast(
device_type = "cuda", dtype = self._autocast_dtype
):
_pg_hidden = unwrapped_model(
input_ids = _pg_layout.flat_ids,
position_ids = _pg_layout.position_ids,
prefix_seg_info = _pg_layout.prefix_seg_info,
use_cache = False,
).logits
_pg_r = _pg_layout.extract_logps(
_pg_hidden,
lm_head,
chunked_hidden_states_selective_log_softmax,
_pg_chunks,
logit_scale_multiply,
logit_scale_divide,
logit_softcapping,
temperature,
)
_pg_hidden = None # release before any verify forward
device_synchronize()
# clip to the loss window [B, logits_to_keep+max_left_pad]
_pg_w = logits_to_keep + max_left_pad
if _pg_r.shape[1] > _pg_w:
_pg_r = _pg_r[:, -_pg_w:]
return _pg_r
# trust only within the verified envelope: re-verify when T or the
# longest segment grows, like the packed path
_pg_T = int(_pg_layout.flat_ids.shape[1])
_pg_maxseg = int(_pg_layout.position_ids.max()) + 1
_pg_env = (
_pg_verified.get(_pg_sig) if isinstance(_pg_verified, dict) else None
)
if (not _pg_verify_on()) or (
_pg_env is not None and _pg_T <= _pg_env[0] and _pg_maxseg <= _pg_env[1]
):
# trusted shape: run PG now and skip the full-row forward below
_pg_result = _pg_run_forward()
_pg_use = True
_pg_skip_pk = True
else:
# unverified shape: defer the forward until the packed reference
# exists (verify site below), so a declined packed path never wastes
# a whole-batch PG forward
_pg_forward_fn = _pg_run_forward
except Exception as _pg_err:
_pg_result = None
_pg_use = False
_pg_skip_pk = False
_pg_forward_fn = None
# A FlexAttention/Triton compile failure or OOM here is GPU-wide, not
# layout-specific, so retrying the same PG forward every step just re-pays
# the failure. Persistently disable PG (mirrors the seq-packing handler
# setting _unsloth_seq_packing_nograd_ok = False); the packed/padded path
# below still produces the exact result.
unwrapped_model._unsloth_prefix_grouper_nograd_disabled = True
if isinstance(_pg_err, torch.cuda.OutOfMemoryError):
torch.cuda.empty_cache()
os.environ["UNSLOTH_RETURN_HIDDEN_STATES"] = "1"
if UNSLOTH_ENABLE_LOGGING:
print(
f"[Unsloth] GRPO PrefixGrouper (no-grad) disabled (fell back to packed): {_pg_err!r}",
flush = True,
)
# ---- Sequence packing (default-on; disable with UNSLOTH_GRPO_SEQ_PACKING=0) ----
# One varlen [1, sum L] block-diagonal forward replaces the padded [B, Lmax] loop
# (exact per-row result; also fixes the padded path's left-pad RoPE error).
# Self-verified vs the per-row forward, re-checked as T grows; falls back if a
# backend ignores packed_seq_lengths. lm_head runs on completion positions only.
_pk_result = None
_pk_use = False
_pk_enabled = UNSLOTH_GRPO_SEQ_PACKING_ON
# Without zoo#840's masked-column guard, zeroed prompt/pad columns turn NaN in exp().
_pk_enabled = _pk_enabled and UNSLOTH_ZOO_HAS_MASKED_COL_GUARD
_pk_ok = getattr(unwrapped_model, "_unsloth_seq_packing_nograd_ok", None)
if (
_pk_enabled
and not _pg_skip_pk
and pixel_values is None
and token_type_ids is None
and mm_token_type_ids is None
and _pk_ok is not False
):
try:
_pk_pad = self.processing_class.pad_token_id
_pk_keep = input_ids != _pk_pad
_pk_len = _pk_keep.sum(dim = 1)
_pk_len_cpu = _pk_len.tolist() # single GPU->CPU sync, reused below
_pk_nz_cpu = [_n for _n in _pk_len_cpu if _n > 0]
_pk_flat = input_ids[_pk_keep].unsqueeze(0)
_pk_T = _pk_flat.shape[1]
_pk_L = input_ids.shape[1]
_pk_W = logits_to_keep + max_left_pad
_pk_maxseg = max(_pk_nz_cpu) if _pk_nz_cpu else 0
# sliding-window models lose the per-sequence local window in a packed stream
_pk_sw = getattr(
getattr(unwrapped_model, "config", None), "sliding_window", None
)
_pk_sw_ok = not (isinstance(_pk_sw, int) and _pk_sw > 0 and _pk_maxseg > _pk_sw)
# per-row completion mask (same as the loss); prompt-only rows count as inactive
_pk_cmask = create_completion_attention_mask(
input_ids[:, -_pk_W:], left_pad_tokens_per_prompt, max_left_pad, _pk_pad
)
_pk_active = int(_pk_cmask.any(dim = 1).sum())
# skip the packed forward entirely at known-unsafe lengths (avoids a wasted pass / OOM)
_pk_unsafe = getattr(
unwrapped_model, "_unsloth_seq_packing_nograd_unsafe_T", None
)
# cap the flattened forward at one padded [batch_size, seq_len] mini-batch's
# token budget; anything larger uses the chunked padded loop
_pk_cap = batch_size * seq_len
if (
_pk_T >= 2
and _pk_T <= _pk_cap
and len(_pk_nz_cpu) > 0
and _pk_sw_ok
and not (_pk_unsafe is not None and _pk_T >= _pk_unsafe)
and (_pk_ok is True or _pk_active >= 2)
):
# reset 0-based position_ids per segment
_pk_pos = (_pk_keep.cumsum(dim = 1) - 1)[_pk_keep].unsqueeze(0)
_pk_chunks = max(1, total_rows * multiplier)
_pk_nz_idx = _pk_keep.nonzero(
as_tuple = False
) # [T, 2] = (row, col), row-major
_pk_within = _pk_nz_idx[1:, 0] == _pk_nz_idx[:-1, 0] # [T-1]
# per-row completion start after left-packing (matches create_completion_attention_mask)
_pk_cstart = (_pk_L - logits_to_keep) - left_pad_tokens_per_prompt # [rows]
_pk_ctgt = (_pk_nz_idx[1:, 1] >= _pk_cstart[_pk_nz_idx[1:, 0]]) & _pk_within
with _get_inference_mode_context_manager(model):
with torch.amp.autocast(device_type = "cuda", dtype = self._autocast_dtype):
# use_cache=False: a KV cache silently disables varlen packing
_pk_hidden = unwrapped_model(
input_ids = _pk_flat,
position_ids = _pk_pos,
packed_seq_lengths = torch.tensor(
_pk_nz_cpu, dtype = torch.int32, device = input_ids.device
),
use_cache = False,
).logits
_pk_sel = chunked_hidden_states_selective_log_softmax(
_pk_hidden[0, :-1, :][_pk_ctgt].unsqueeze(0),
lm_head,
_pk_flat[0, 1:][_pk_ctgt].unsqueeze(0),
_pk_chunks,
logit_scale_multiply,
logit_scale_divide,
logit_softcapping,
temperature,
)[0]
# GPT-OSS offload race guard (matches the padded loop)
device_synchronize()
# scatter each logprob back to its (row, col) so [:, -_pk_W:] matches padded
_pk_tgt = (_pk_nz_idx[1:, 0] * _pk_L + _pk_nz_idx[1:, 1])[_pk_ctgt]
_pk_result = (
torch.zeros(
total_rows * _pk_L,
dtype = torch.float32,
device = input_ids.device,
)
.index_put((_pk_tgt,), _pk_sel.to(torch.float32))
.view(total_rows, _pk_L)[:, -_pk_W:]
)
# re-verify when T or the longest segment grows past what was verified
# (a LongRoPE cache switch can change the result)
_pk_vT = int(
getattr(unwrapped_model, "_unsloth_seq_packing_nograd_verified_T", 0)
)
_pk_vS = int(
getattr(unwrapped_model, "_unsloth_seq_packing_nograd_verified_seg", 0)
)
# debug: hand-edit this condition to force re-verify every step
if _pk_ok is True and _pk_T <= _pk_vT and _pk_maxseg <= _pk_vS:
_pk_use = True # already verified for this shape
else:
# verify against the per-row forward (ground truth)
_pk_ref = torch.zeros_like(_pk_result)
with _get_inference_mode_context_manager(model):
with torch.amp.autocast(
device_type = "cuda", dtype = self._autocast_dtype
):
for _pk_i in range(total_rows):
_pk_ni = _pk_len_cpu[_pk_i]
if _pk_ni < 2:
continue
_pk_rmask = _pk_keep[_pk_i]
_pk_real = input_ids[_pk_i][_pk_rmask].unsqueeze(0)
_pk_rpos = torch.arange(
_pk_ni, device = input_ids.device
).unsqueeze(0)
_pk_rh = unwrapped_model(
input_ids = _pk_real,
position_ids = _pk_rpos,
use_cache = False,
).logits
_pk_rsel = chunked_hidden_states_selective_log_softmax(
_pk_rh[:, :-1, :],
lm_head,
_pk_real[:, 1:],
1,
logit_scale_multiply,
logit_scale_divide,
logit_softcapping,
temperature,
)[0]
_pk_rcols = _pk_rmask.nonzero(as_tuple = False).squeeze(1)[
1:
] - (_pk_L - _pk_W)
_pk_rkeep = _pk_rcols >= 0
_pk_ref[_pk_i, _pk_rcols[_pk_rkeep]] = _pk_rsel[
_pk_rkeep
].to(torch.float32)
device_synchronize()
# compare over the loss-mask region only
_pk_cm = _pk_cmask.float()
_pk_diff = float(((_pk_result - _pk_ref).abs() * _pk_cm).max())
if UNSLOTH_ENABLE_LOGGING:
print(
f"[Unsloth] GRPO seq-packing (no-grad) verify: T={_pk_T} maxseg={_pk_maxseg} packed-vs-perrow max|d|={_pk_diff:.4f}",
flush = True,
)
# kernel-noise floor ~0.25; cross-sample contamination is >= 2.4
if _pk_diff < 7e-1:
unwrapped_model._unsloth_seq_packing_nograd_ok = True
# widen the trusted shape only when >= 2 completion rows exercised
# cross-sample packing; single-row passes prove nothing
if _pk_active >= 2:
unwrapped_model._unsloth_seq_packing_nograd_verified_T = max(
_pk_vT, _pk_T
)
unwrapped_model._unsloth_seq_packing_nograd_verified_seg = max(
_pk_vS, _pk_maxseg
)
_pk_ok = True
_pk_use = True
else:
_pk_use = False
if _pk_diff >= 1.5:
# contamination (attention ignores the packed mask): disable packing
unwrapped_model._unsloth_seq_packing_nograd_ok = False
else:
# likely a length boundary (LongRoPE): mark unsafe, keep smaller shapes
unwrapped_model._unsloth_seq_packing_nograd_unsafe_T = (
_pk_T if _pk_unsafe is None else min(_pk_unsafe, _pk_T)
)
if UNSLOTH_ENABLE_LOGGING:
print(
f"[Unsloth] GRPO seq-packing (no-grad) fell back at T={_pk_T} (diff={_pk_diff:.3f})",
flush = True,
)
except Exception as _pk_err:
# any failure: drop intermediates, use the padded loop, do not retry
_pk_hidden = None
_pk_sel = None
_pk_result = None
_pk_use = False
if isinstance(_pk_err, torch.cuda.OutOfMemoryError):
torch.cuda.empty_cache()
unwrapped_model._unsloth_seq_packing_nograd_ok = False
if UNSLOTH_ENABLE_LOGGING:
print(
f"[Unsloth] GRPO sequence-packing (no-grad) disabled (fell back to padded): {_pk_err!r}",
flush = True,
)
# ---- PrefixGrouper first-use self-verify (no-grad) ----
# Compare the untrusted PG result to the full-row packed result (itself verified vs
# per-row) over the completion mask: < tol_ok -> trust the structure; >= TOL_KILL ->
# unsafe forever; borderline -> fall back this shape.
if _pg_forward_fn is not None and not _pg_use:
if _pk_use and _pk_result is not None:
try:
# deferred PG forward, run only now that the packed reference exists
_pg_result = _pg_forward_fn()
_pg_W2 = logits_to_keep + max_left_pad
_pg_cm = create_completion_attention_mask(
input_ids[:, -_pg_W2:],
left_pad_tokens_per_prompt,
max_left_pad,
self.processing_class.pad_token_id,
).float()
_pg_a = _pg_result[:, -_pg_W2:].float()
_pg_b = _pk_result[:, -_pg_W2:].float()
_pg_diff = float(((_pg_a - _pg_b).abs() * _pg_cm).max())
if UNSLOTH_ENABLE_LOGGING:
print(
f"[Unsloth] GRPO PrefixGrouper (no-grad) verify: sig={_pg_layout.signature} "
f"shared-prefix vs full-row-packed max|d|={_pg_diff:.4f}",
flush = True,
)
if _pg_diff < _pg_tol_ok():
_pg_v = getattr(
unwrapped_model, "_unsloth_prefix_grouper_nograd_verified", None
)
if not isinstance(_pg_v, dict):
_pg_v = {}
_pg_vT = int(_pg_layout.flat_ids.shape[1])
_pg_vS = int(_pg_layout.position_ids.max()) + 1
_pg_old = _pg_v.get(_pg_layout.signature, (0, 0))
_pg_v[_pg_layout.signature] = (
max(_pg_vT, _pg_old[0]),
max(_pg_vS, _pg_old[1]),
)
unwrapped_model._unsloth_prefix_grouper_nograd_verified = _pg_v
_pg_use = True
else:
_pg_u = getattr(
unwrapped_model, "_unsloth_prefix_grouper_nograd_unsafe", None
)
if _pg_u is None:
_pg_u = set()
if _pg_diff >= _PG_TOL_KILL:
_pg_u.add(_pg_layout.signature)
unwrapped_model._unsloth_prefix_grouper_nograd_unsafe = _pg_u
_pg_use = False
except Exception as _pg_err3:
_pg_result = None
_pg_use = False
if isinstance(_pg_err3, torch.cuda.OutOfMemoryError):
torch.cuda.empty_cache()
os.environ["UNSLOTH_RETURN_HIDDEN_STATES"] = "1"
if UNSLOTH_ENABLE_LOGGING:
print(
f"[Unsloth] GRPO PrefixGrouper (no-grad) verify failed (fell back to packed): {_pg_err3!r}",
flush = True,
)
# else: no packed reference (packing off/failed) -> cannot verify; fall back.
if _pg_use and _pg_result is not None:
logprobs = _pg_result # PrefixGrouper verified/trusted -> skip the loop
zipped_inputs = []
elif _pk_use and _pk_result is not None:
logprobs = _pk_result # verified -> skip the loop
zipped_inputs = []
else:
# free packed intermediates before running the padded loop
_pk_hidden = _pk_sel = _pk_result = _pk_ref = None
with _get_inference_mode_context_manager(model):
for (
input_ids_chunk,
attention_mask_chunk,
pixel_values_chunk,
image_grid_thw_chunk,
pixel_attention_mask_chunk,
image_sizes_chunk,
token_type_ids_chunk,
mm_token_type_ids_chunk,
) in zipped_inputs:
_extra_vision_kwargs = {}
if token_type_ids_chunk is not None:
_extra_vision_kwargs["token_type_ids"] = token_type_ids_chunk
if mm_token_type_ids_chunk is not None:
_extra_vision_kwargs["mm_token_type_ids"] = mm_token_type_ids_chunk
with torch.amp.autocast(device_type = "cuda", dtype = self._autocast_dtype):
if pixel_values is None:
outputs = unwrapped_model(
input_ids = input_ids_chunk,
attention_mask = attention_mask_chunk,
pixel_values = pixel_values_chunk,
image_grid_thw = image_grid_thw_chunk,
pixel_attention_mask = pixel_attention_mask_chunk,
image_sizes = image_sizes_chunk,
**_extra_vision_kwargs,
)
logits_chunk = outputs.logits
del outputs # free hidden_states before chunked log-softmax
completion_input_ids_chunk = input_ids_chunk[
:, -(logits_to_keep + max_left_pad) :
]
logits_chunk = logits_chunk[
:, -(logits_to_keep + max_left_pad + 1) :, :
]
logits_chunk = logits_chunk[:, :-1, :]
logprobs_chunk = chunked_hidden_states_selective_log_softmax(
logits_chunk,
lm_head,
completion_input_ids_chunk,
chunks = input_ids_chunk.shape[0] * multiplier,
logit_scale_multiply = logit_scale_multiply,
logit_scale_divide = logit_scale_divide,
logit_softcapping = logit_softcapping,
temperature = temperature,
)
else:
# Essentially, for VLMs we do not go via the optimized path in models/,
# so we don't encounter the Flash Attn left-padding issue.
outputs = unwrapped_model(
input_ids = input_ids_chunk,
attention_mask = attention_mask_chunk,
pixel_values = pixel_values_chunk,
image_grid_thw = image_grid_thw_chunk,
pixel_attention_mask = pixel_attention_mask_chunk,
image_sizes = image_sizes_chunk,
logits_to_keep = logits_to_keep + 1,
**_extra_vision_kwargs,
)
logits_chunk = outputs.logits
del outputs # free hidden_states before chunked log-softmax
logits_chunk = logits_chunk[:, :-1, :]
completion_input_ids_chunk = input_ids_chunk[:, -logits_to_keep:]
# Guard: check if model returned hidden states or logits
if logits_chunk.shape[-1] == lm_head.shape[1]:
logprobs_chunk = chunked_hidden_states_selective_log_softmax(
logits_chunk,
lm_head,
completion_input_ids_chunk,
chunks = input_ids_chunk.shape[0] * multiplier,
logit_scale_multiply = logit_scale_multiply,
logit_scale_divide = logit_scale_divide,
logit_softcapping = logit_softcapping,
temperature = temperature,
)
else:
# Model returned logits directly - scaling/softcapping already applied by model forward
logprobs_chunk = chunked_selective_log_softmax(
logits_chunk,
completion_input_ids_chunk,
temperature,
)
# This is needed to avoid race conditions with GPT OSS offload_embbed=True
# However, it seems that this line does not slow down or disrupt models.
device_synchronize()
all_logprobs_list.append(logprobs_chunk)
if logprobs is None: # padded fallback when packing was not used
logprobs = torch.cat(all_logprobs_list, dim = 0)
entropies = None
os.environ["UNSLOTH_RETURN_HIDDEN_STATES"] = "0"
# aux loss is unused: it is off by default (router_aux_loss_coef set to 0 in models/rl.py)
# and explicit opt-in is rejected at trainer init, so this is always None (kept in the
# return for TRL >= 1.7.0's 3-tuple contract).
aux_loss = None
return logprobs.detach(), entropies, aux_loss # logps, entropies, aux_loss
# input_ids = input_ids[:, -logits_to_keep:]
# For transformers<=4.48, logits_to_keep argument isn't supported, so here we drop logits ourselves.
# See https://github.com/huggingface/trl/issues/2770
# logits = logits[:, -logits_to_keep:]
# return logits
# See https://huggingface.co/blog/the_n_implementation_details_of_rlhf_with_ppo#policy-training-implementation-details
# logits = logits / self.temperature
# logps = selective_log_softmax(logits, input_ids)
# row_indices, col_indices = torch.where(logps < -20)
# # Method 1: Check if tensors have elements
# if len(row_indices) > 0 and len(col_indices) > 0:
# breakpoint() # Breakpoint triggered here
# print("Found high values!")
# return logps # compute logprobs for the input tokens
function = inspect.getsource(_get_per_token_logps_and_entropies)
if trl_version < Version("1.7.0"):
# TRL < 1.7.0 unpacks (logps, entropies) at every call site; TRL >= 1.7.0
# always unpacks (logps, entropies, aux_loss). Drop the aux_loss element so
# the return arity matches the installed TRL. Regex tolerates comment /
# whitespace drift on the return line; fail loud if the anchor ever stops
# matching rather than silently shipping a 3-tuple to older TRL.
new_function, n = re.subn(
r"return (logprobs\.detach\(\), entropies), aux_loss[^\n]*",
r"return \1 # logps, entropies",
function,
)
if n != 1:
raise RuntimeError(
"Unsloth GRPO: could not downgrade the per-token-logps return to a "
f"2-tuple for TRL {trl_version} (matched {n} times, expected 1). The "
"return line changed; update the arity gate in rl_replacements.py."
)
function = new_function
return function
RL_FUNCTIONS["grpo_trainer"].append(grpo_trainer__get_per_token_logps_and_entropies)
def _unsloth_get_model_config(model):
"""Return HuggingFace model config, unwrapping DDP/Accelerate wrappers."""
config = getattr(model, "config", None)
if config is None and hasattr(model, "module"):
config = getattr(model.module, "config", None)
return config
def _unsloth_get_final_logit_softcapping(model):
"""Return final_logit_softcapping for a model config, falling back to the
nested text sub-config for composite models. Handles both:
- Gemma-4-style configs where the attribute lives on ``config.text_config``
- T5Gemma-style composite configs where the text sub-config is only
reachable via ``config.get_text_config()``
Returns 0 if unset, matching the previous behaviour.
"""
config = _unsloth_get_model_config(model)
if config is None:
return 0
softcap = getattr(config, "final_logit_softcapping", None)
if softcap is None:
text_cfg = getattr(config, "text_config", None)
if text_cfg is None:
get_text_config = getattr(config, "get_text_config", None)
if callable(get_text_config):
try:
text_cfg = get_text_config()
except (TypeError, ValueError):
text_cfg = None
if text_cfg is not None and text_cfg is not config:
softcap = getattr(text_cfg, "final_logit_softcapping", None)
return 0 if softcap is None else softcap
grpo_compute_loss = RL_REPLACEMENTS["grpo_compute_loss"]
grpo_compute_loss_slow = RL_REPLACEMENTS["grpo_compute_loss_slow"]
UnslothEfficientGRPO = RL_REPLACEMENTS["UnslothEfficientGRPO"]
grpo_accumulated_loss = RL_REPLACEMENTS["grpo_accumulated_loss"]
grpo_update_SamplingParams = RL_REPLACEMENTS["grpo_update_SamplingParams"]
RL_PRE_ITEMS["grpo_trainer"].append(inspect.getsource(_unsloth_get_model_config))
RL_PRE_ITEMS["grpo_trainer"].append(inspect.getsource(_unsloth_get_final_logit_softcapping))
RL_PRE_ITEMS["grpo_trainer"].append(inspect.getsource(_unsloth_get_mm_token_id))
RL_PRE_ITEMS["grpo_trainer"].append(inspect.getsource(_unsloth_fix_mm_token_type_ids))
RL_PRE_ITEMS["grpo_trainer"].append(inspect.getsource(_unsloth_clear_stateful_mrope))
RL_PRE_ITEMS["grpo_trainer"].append(inspect.getsource(grpo_compute_loss))
RL_PRE_ITEMS["grpo_trainer"].append(inspect.getsource(UnslothEfficientGRPO))
RL_PRE_ITEMS["grpo_trainer"].append(inspect.getsource(grpo_accumulated_loss))
RL_PRE_ITEMS["grpo_trainer"].append(grpo_compute_loss_slow)
RL_PRE_ITEMS["grpo_trainer"].append(inspect.getsource(grpo_update_SamplingParams))
RL_PRE_ITEMS["grpo_trainer"].append(inspect.getsource(_get_inference_mode_context_manager))
# inspect.getsource inlines function bodies but not module imports, so constants the inlined
# grpo functions reference (e.g. UNSLOTH_ENABLE_LOGGING) must be redefined in the generated cache.
RL_PRE_ITEMS["grpo_trainer"].append(
"import os as _unsloth_os\n"
"UNSLOTH_ENABLE_LOGGING = _unsloth_os.environ.get('UNSLOTH_ENABLE_LOGGING', '0') in ('1', 'True', 'true')\n"
)
# Sequence-packing gates, same values as the module-top constants.
RL_PRE_ITEMS["grpo_trainer"].append(
"UNSLOTH_GRPO_SEQ_PACKING_ON = _unsloth_os.environ.get('UNSLOTH_GRPO_SEQ_PACKING', '1').lower() not in ('0', 'false', 'no', 'off')\n"
)
RL_PRE_ITEMS["grpo_trainer"].append(
"try:\n"
" import inspect as _unsloth_inspect\n"
" from unsloth_zoo.rl_replacements import RL_REPLACEMENTS as _unsloth_zoo_RL\n"
" UNSLOTH_ZOO_HAS_MASKED_COL_GUARD = 'torch.where(_keep, new' in _unsloth_inspect.getsource(_unsloth_zoo_RL['grpo_compute_loss'])\n"
"except Exception:\n"
" UNSLOTH_ZOO_HAS_MASKED_COL_GUARD = False\n"
)
# PrefixGrouper gate, same shape as the module-top constants.
RL_PRE_ITEMS["grpo_trainer"].append(
"_pg_build_layout = _pg_enabled_fn = _pg_verify_on = _pg_tol_ok = _PG_TOL_KILL = None\n"
"UNSLOTH_GRPO_PREFIX_GROUPER_ON = _unsloth_os.environ.get('UNSLOTH_GRPO_PREFIX_GROUPER', '1').lower() not in ('0', 'false', 'no', 'off')\n"
"if UNSLOTH_GRPO_PREFIX_GROUPER_ON:\n"
" try:\n"
" from unsloth.utils.prefix_grouper import build_group_layout as _pg_build_layout, prefix_grouper_enabled as _pg_enabled_fn, verify_on as _pg_verify_on, tol_ok as _pg_tol_ok, TOL_KILL as _PG_TOL_KILL\n"
" except Exception:\n"
" UNSLOTH_GRPO_PREFIX_GROUPER_ON = False\n"
)
# Edit _get_per_token_logps to handle mixed precision
def grpo_trainer_compute_loss(function_name, function):
if function_name != "compute_loss":
return function
def compute_loss(
self,
model,
inputs,
return_outputs = False,
num_items_in_batch = None,
):
if return_outputs:
raise ValueError("The GRPOTrainer does not support returning outputs")
# Compute the per-token log probabilities for the model
prompt_ids, prompt_mask = inputs["prompt_ids"], inputs["prompt_mask"]
completion_ids, completion_mask = (
inputs["completion_ids"],
inputs["completion_mask"],
)
pixel_values, image_grid_thw = (
inputs.get("pixel_values", None),
inputs.get("image_grid_thw", None),
)
pixel_attention_mask, image_sizes = (
inputs.get("pixel_attention_mask", None),
inputs.get("image_sizes", None),
)
num_images = inputs.get("num_images", None)
# Transformers 5.x needs token_type_ids/mm_token_type_ids for some vision models
token_type_ids = inputs.get("token_type_ids", None)
mm_token_type_ids = inputs.get("mm_token_type_ids", None)
num_items_in_batch = inputs.get("num_items_in_batch", None)
sampling_per_token_logps = inputs.get("sampling_per_token_logps", None)
tool_mask = inputs.get("tool_mask", None)
# Missing when evaluate() runs standalone; eval does not accumulate, so
# fall back to 1 to avoid underreporting eval_loss (#2464).
current_gradient_accumulation_steps = getattr(
self, "current_gradient_accumulation_steps", 1
)
num_processes = self.accelerator.num_processes
input_ids = torch.cat([prompt_ids, completion_ids], dim = 1)
bsz, qlen = input_ids.shape
attention_mask = torch.cat([prompt_mask, completion_mask], dim = 1)
if mm_token_type_ids is not None or image_grid_thw is not None:
mm_token_type_ids = _unsloth_fix_mm_token_type_ids(
self.processing_class,
input_ids,
mm_token_type_ids,
completion_ids = completion_ids,
)
# attention_mask = None
logits_to_keep = completion_ids.size(
1
) # we only need to compute the logits for the completion tokens
_input_ids = input_ids
_logits_to_keep = logits_to_keep
get_logps_func = (
lambda model,
input_ids,
attention_mask,
logits_to_keep,
batch_size = None,
compute_entropy = False,
compute_efficient = False: (
self._get_per_token_logps(
model, input_ids, attention_mask, logits_to_keep, compute_efficient
)
if hasattr(self, "_get_per_token_logps")
else self._get_per_token_logps_and_entropies(
model,
input_ids,
attention_mask,
logits_to_keep,
batch_size,
compute_entropy,
compute_efficient,
)[0]
)
) # logps
per_token_logps = get_logps_func(
model, input_ids, attention_mask, logits_to_keep, compute_efficient = True
)
# Compute the KL divergence between the model and the reference model
# _prepare_inputs doesn't return reference log probs anymore. We need to calculate it ourselves.
# https://github.com/huggingface/trl/blob/05bc43e960396581e458195b8388efe6b82cae1f/trl/trainer/grpo_trainer.py#L1328
# if self.beta != 0.0:
# with torch.inference_mode(), model.disable_adapter():
# ref_per_token_logps = per_token_logps = get_logps_func(model, input_ids, attention_mask, logits_to_keep)
# else:
# ref_per_token_logps = None
ref_logps = inputs.get("ref_per_token_logps", None)
# per_token_kl = torch.exp(ref_per_token_logps - per_token_logps) - (ref_per_token_logps - per_token_logps) - 1
# x - x.detach() allows for preserving gradients from x
advantages = inputs["advantages"]
# per_token_loss = torch.exp(per_token_logps - per_token_logps.detach()) * advantages.unsqueeze(1)
# per_token_loss = -(per_token_loss - self.beta * per_token_kl)
# loss = ((per_token_loss * completion_mask).sum(dim=1) / completion_mask.sum(dim=1)).mean()
old_logps = inputs.get("old_per_token_logps", None)
input_ids = input_ids[:, -logits_to_keep:]
# Get logit softcapping and logit scale
model_config = _unsloth_get_model_config(model)
logit_softcapping = _unsloth_get_final_logit_softcapping(model) # Gemma
logit_scale_multiply = getattr(model_config, "logit_scale", 0) # Cohere
if logit_scale_multiply is None:
logit_scale_multiply = 0
logit_scale_divide = getattr(model_config, "logits_scaling", 0) # Granite
if logit_scale_divide is None:
logit_scale_divide = 0
max_left_pad = inputs.get("max_left_pad", 0)
if per_token_logps is not None:
loss_mask = completion_mask
if tool_mask is not None:
if tool_mask.shape != completion_mask.shape:
raise ValueError(
"tool_mask/env_mask must have the same shape as completion_mask"
)
loss_mask = completion_mask * tool_mask.to(
device = completion_mask.device,
dtype = completion_mask.dtype,
)
(
loss,
completion_length,
mean_kl,
delta,
flat_is_ratio,
coef_1,
completion_mask,
) = grpo_compute_loss_slow(
ref_logps,
per_token_logps,
old_logps,
sampling_per_token_logps,
input_ids,
loss_mask,
self.beta,
advantages,
pixel_values = pixel_values,
image_grid_thw = image_grid_thw,
loss_type = self.args.loss_type,
importance_sampling_level = self.importance_sampling_level,
epsilon_low = self.epsilon_low,
epsilon_high = self.epsilon_high,
max_completion_length = self.args.max_completion_length,
delta = self.args.delta,
temperature = self.args.temperature,
max_left_pad = max_left_pad,
logit_softcapping = logit_softcapping,
logit_scale_multiply = logit_scale_multiply,
logit_scale_divide = logit_scale_divide,
num_items_in_batch = num_items_in_batch,
current_gradient_accumulation_steps = current_gradient_accumulation_steps,
num_processes = num_processes,
)
else:
def _unsloth_requires_multi_image_zoo(value):
if value is None:
return False
if isinstance(value, torch.Tensor):
counts = value.detach().cpu().reshape(-1).tolist()
else:
counts = list(value)
return any(int(n) != 1 for n in counts)
if _unsloth_requires_multi_image_zoo(num_images) and not getattr(
self, "_unsloth_grpo_zoo_checked", False
):
_supports_num_images = (
"num_images" in inspect.signature(grpo_accumulated_loss).parameters
)
if not _supports_num_images:
try:
_zoo_src = inspect.getsource(grpo_accumulated_loss)
except (TypeError, OSError):
_zoo_src = ""
_supports_num_images = "num_images" in _zoo_src
if not _supports_num_images:
raise RuntimeError(
"Multi-image GRPO requires an unsloth_zoo build whose "
"grpo_accumulated_loss handles num_images. Please upgrade "
"unsloth_zoo (see https://github.com/unslothai/unsloth-zoo/pull/613)."
)
self._unsloth_grpo_zoo_checked = True
if tool_mask is not None and not getattr(
self, "_unsloth_grpo_tool_mask_zoo_checked", False
):
_supports_tool_mask = (
"tool_mask" in inspect.signature(grpo_accumulated_loss).parameters
)
if not _supports_tool_mask:
try:
_zoo_src = inspect.getsource(grpo_accumulated_loss)
except (TypeError, OSError):
_zoo_src = ""
_supports_tool_mask = "tool_mask" in _zoo_src
if not _supports_tool_mask:
raise RuntimeError(
"env_mask/tool_mask GRPO requires an unsloth_zoo build whose "
"grpo_accumulated_loss handles tool_mask. Please upgrade "
"unsloth_zoo."
)
self._unsloth_grpo_tool_mask_zoo_checked = True
_grpo_accumulated_loss_kwargs = {}
if tool_mask is not None:
_grpo_accumulated_loss_kwargs["tool_mask"] = tool_mask
if hasattr(self.args, "loss_type"):
(
loss,
completion_length,
mean_kl,
delta,
flat_is_ratio,
coef_1,
completion_mask,
) = grpo_accumulated_loss(
trainer = self,
input_ids = _input_ids,
pixel_values = pixel_values,
image_grid_thw = image_grid_thw,
pixel_attention_mask = pixel_attention_mask,
image_sizes = image_sizes,
num_images = num_images,
logits_to_keep = logits_to_keep,
completion_mask = completion_mask,
advantages = advantages,
old_logps = old_logps,
ref_logps = ref_logps,
n_chunks = self.args.unsloth_num_chunks,
loss_type = self.args.loss_type,
importance_sampling_level = self.importance_sampling_level,
epsilon_low = self.epsilon_low,
epsilon_high = self.epsilon_high,
max_completion_length = self.args.max_completion_length,
delta = self.args.delta,
temperature = self.args.temperature,
max_left_pad = max_left_pad,
logit_softcapping = logit_softcapping,
logit_scale_multiply = logit_scale_multiply,
logit_scale_divide = logit_scale_divide,
attention_mask = attention_mask,
num_items_in_batch = num_items_in_batch,
current_gradient_accumulation_steps = current_gradient_accumulation_steps,
num_processes = num_processes,
sampling_per_token_logps = sampling_per_token_logps,
token_type_ids = token_type_ids,
mm_token_type_ids = mm_token_type_ids,
**_grpo_accumulated_loss_kwargs,
)
else:
# to ensure backwards compatibility with trl 0.15.2 and maybe even 0.17
loss, completion_length, mean_kl, coef_1, completion_mask = grpo_accumulated_loss(
trainer = self,
input_ids = _input_ids,
pixel_values = pixel_values,
image_grid_thw = image_grid_thw,
pixel_attention_mask = pixel_attention_mask,
image_sizes = image_sizes,
num_images = num_images,
logits_to_keep = logits_to_keep,
completion_mask = completion_mask,
advantages = advantages,
old_logps = old_logps,
ref_logps = ref_logps,
n_chunks = self.args.unsloth_num_chunks,
temperature = self.args.temperature,
logit_softcapping = logit_softcapping,
logit_scale_multiply = logit_scale_multiply,
logit_scale_divide = logit_scale_divide,
attention_mask = attention_mask,
token_type_ids = token_type_ids,
mm_token_type_ids = mm_token_type_ids,
**_grpo_accumulated_loss_kwargs,
)
if "train" in self._metrics:
mode = "eval" if self.control.should_evaluate else "train"
self._metrics[mode]["completion_length"].append(completion_length.item())
self._metrics[mode]["kl"].append(mean_kl.item())
else:
self._metrics["completion_length"].append(completion_length.item())
self._metrics["kl"].append(mean_kl.item())
if (
self.use_vllm
and delta is not None
and getattr(self, "vllm_importance_sampling_correction", False)
):
mean_delta = (
torch.mean(delta)
if delta.numel() > 0
else torch.tensor(0.0, device = self.model.device)
)
max_delta = (
torch.max(delta)
if delta.numel() > 0
else torch.tensor(0.0, device = self.model.device)
)
self._metrics[mode]["sampling/sampling_logp_difference/mean"].append(
self.accelerator.gather(mean_delta).mean().item()
)
self._metrics[mode]["sampling/sampling_logp_difference/max"].append(
self.accelerator.gather(max_delta).max().item()
)
min_importance_sampling_ratio = (
torch.min(flat_is_ratio)
if flat_is_ratio.numel() > 0
else torch.tensor(0.0, device = self.model.device)
)
mean_importance_sampling_ratio = (
torch.mean(flat_is_ratio)
if flat_is_ratio.numel() > 0
else torch.tensor(0.0, device = self.model.device)
)
max_importance_sampling_ratio = (
torch.max(flat_is_ratio)
if flat_is_ratio.numel() > 0
else torch.tensor(0.0, device = self.model.device)
)
self._metrics[mode]["sampling/importance_sampling_ratio/min"].append(
self.accelerator.gather(min_importance_sampling_ratio)
.nan_to_num(nan = float("inf"))
.min()
.item()
)
self._metrics[mode]["sampling/importance_sampling_ratio/mean"].append(
self.accelerator.gather(mean_importance_sampling_ratio).nanmean().item()
)
self._metrics[mode]["sampling/importance_sampling_ratio/max"].append(
self.accelerator.gather(max_importance_sampling_ratio)
.nan_to_num(nan = float("-inf"))
.max()
.item()
)
completion_token_count = completion_mask.sum().clamp(min = 1.0)
def masked_batch_mean(x):
if x.shape[1] == 1: # when importance_sampling_level == "sequence"
return x.mean()
else:
return (x * completion_mask).sum() / completion_token_count
if advantages.dim() == 1:
advantages = advantages.unsqueeze(1)
if self.loss_type in ["grpo", "bnpo", "dr_grpo", "dapo"]:
# Compute the clipped probability ratios
is_low_clipped = (coef_1 < 1 - self.epsilon_low) & (advantages < 0)
is_high_clipped = (coef_1 > 1 + self.epsilon_high) & (advantages > 0)
is_region_clipped = is_low_clipped | is_high_clipped
low_clip = masked_batch_mean(is_low_clipped.float())
high_clip = masked_batch_mean(is_high_clipped.float())
clip_ratio = masked_batch_mean(is_region_clipped.float())
gathered_low_clip = self.accelerator.gather(low_clip)
self._metrics[mode]["clip_ratio/low_mean"].append(gathered_low_clip.nanmean().item())
self._metrics[mode]["clip_ratio/low_min"].append(nanmin(gathered_low_clip).item())
gathered_high_clip = self.accelerator.gather(high_clip)
self._metrics[mode]["clip_ratio/high_mean"].append(gathered_high_clip.nanmean().item())
self._metrics[mode]["clip_ratio/high_max"].append(nanmax(gathered_high_clip).item())
gathered_clip_ratio = self.accelerator.gather(clip_ratio)
self._metrics[mode]["clip_ratio/region_mean"].append(
gathered_clip_ratio.nanmean().item()
)
elif self.loss_type == "cispo":
is_cispo_clipped = (coef_1 > self.epsilon_high) & (advantages > 0)
cispo_clip_ratio = masked_batch_mean(is_cispo_clipped.float())
gathered_cispo_clip_ratio = self.accelerator.gather(cispo_clip_ratio)
self._metrics[mode]["cispo_clip_ratio"].append(
gathered_cispo_clip_ratio.nanmean().item()
)
return loss
function = inspect.getsource(compute_loss)
return function
RL_FUNCTIONS["grpo_trainer"].append(grpo_trainer_compute_loss)
# Fix KTO shape mismatch when Unsloth model forward truncates input_ids
# but labels aren't truncated. TRL 0.27.2+ _process_tokens only truncates
# completions, not prompts -- so prompts exceeding max_seq_length cause the
# model to produce shorter logits than the labels expect.
def kto_trainer_get_batch_logps(function_name, function):
if function_name != "get_batch_logps":
return function
# The raise is inside an if block inside the method, so we need
# to preserve the exact indentation of the raise statement.
old = 'raise ValueError("Logits (batch and sequence length dim) and labels must have the same shape.")'
new = (
"# Unsloth: auto-truncate to shorter sequence length (model may have truncated input_ids)\n"
" _min_len = min(logits.shape[1], labels.shape[1])\n"
" logits = logits[:, :_min_len, :]\n"
" labels = labels[:, :_min_len]"
)
function = function.replace(old, new)
return function
RL_FUNCTIONS["kto_trainer"].append(kto_trainer_get_batch_logps)
# TRL 1.x dropped KTOTrainer.get_batch_logps and moved the log-prob math into
# _compute_logps / compute_ref_log_probs / _compute_kl_logps, which call
# selective_log_softmax on completion-only tokens. Same truncation hazard as
# above, so clamp logits/ids/mask to the shorter seq length (no-op when equal).
_KTO_COMPLETION_RE = re.compile(
r"(?P<ws>[ \t]*)shift_logits = completion_logits\[:, :-1, :\]\.contiguous\(\)\n"
r"(?P=ws)per_token_logps = selective_log_softmax\(\s*shift_logits,\s*"
r"(?P<var>\w+)\[[\"']completion_input_ids[\"']\]\[:, 1:\]\.contiguous\(\)\s*\)\n"
r"(?P=ws)per_token_logps\[(?P=var)\[[\"']completion_mask[\"']\]\[:, 1:\] == 0\] = 0\.0"
)
_KTO_KL_RE = re.compile(
r"(?P<ws>[ \t]*)shift_KL_logits = KL_logits\[:, :-1, :\]\.contiguous\(\)\n"
r"(?P=ws)KL_per_token_logps = selective_log_softmax\(\s*shift_KL_logits,\s*"
r"(?P<var>\w+)\[[\"']KL_completion_input_ids[\"']\]\[:, 1:\]\.contiguous\(\)\s*\)\n"
r"(?P=ws)KL_per_token_logps\[(?P=var)\[[\"']KL_completion_mask[\"']\]\[:, 1:\] == 0\] = 0\.0"
)
def _kto_completion_repl(m):
ws, var = m.group("ws"), m.group("var")
return (
f"{ws}shift_logits = completion_logits[:, :-1, :].contiguous()\n"
f"{ws}# Unsloth: clamp logits/ids/mask to shorter seq len (model may truncate input_ids)\n"
f'{ws}_uns_ids = {var}["completion_input_ids"][:, 1:].contiguous()\n'
f"{ws}_uns_n = min(shift_logits.shape[1], _uns_ids.shape[1])\n"
f"{ws}per_token_logps = selective_log_softmax(shift_logits[:, :_uns_n], _uns_ids[:, :_uns_n])\n"
f'{ws}per_token_logps[{var}["completion_mask"][:, 1:][:, :_uns_n] == 0] = 0.0'
)
def _kto_kl_repl(m):
ws, var = m.group("ws"), m.group("var")
return (
f"{ws}shift_KL_logits = KL_logits[:, :-1, :].contiguous()\n"
f"{ws}# Unsloth: clamp logits/ids/mask to shorter seq len (model may truncate input_ids)\n"
f'{ws}_uns_kl_ids = {var}["KL_completion_input_ids"][:, 1:].contiguous()\n'
f"{ws}_uns_kl_n = min(shift_KL_logits.shape[1], _uns_kl_ids.shape[1])\n"
f"{ws}KL_per_token_logps = selective_log_softmax(shift_KL_logits[:, :_uns_kl_n], _uns_kl_ids[:, :_uns_kl_n])\n"
f'{ws}KL_per_token_logps[{var}["KL_completion_mask"][:, 1:][:, :_uns_kl_n] == 0] = 0.0'
)
def kto_trainer_align_completion_logps(function_name, function):
if function_name not in (
"_compute_logps",
"compute_ref_log_probs",
"_compute_kl_logps",
):
return function
function = _KTO_COMPLETION_RE.sub(_kto_completion_repl, function)
function = _KTO_KL_RE.sub(_kto_kl_repl, function)
return function
RL_FUNCTIONS["kto_trainer"].append(kto_trainer_align_completion_logps)
# https://github.com/huggingface/trl/blob/main/trl/trainer/grpo_trainer.py#L356
# TRL warns if batch size is not a multiple of num_generations -> fix this.
def grpo_trainer_fix_batch_size(RLTrainer_source, RLConfig_source):
if "divisible by the number of generations" not in RLTrainer_source:
# in later trl versions this doesn't exist anymore
return ""
if "num_generations" not in RLConfig_source:
return ""
check_batch_size = (
"div = per_device_train_batch_size // num_generations\n"
"if div * num_generations != per_device_train_batch_size:\n"
" print('Unsloth: We now expect `per_device_train_batch_size` to be a multiple of `num_generations`.\\n"
"We will change the batch size of ' + str(per_device_train_batch_size) + ' to the `num_generations` of ' + str(num_generations))\n"
" per_device_train_batch_size = num_generations\n"
)
return check_batch_size
RL_CONFIG_CHANGES["grpo_trainer"].append(grpo_trainer_fix_batch_size)
# Add other reward function names
def grpo_trainer_metrics(RLTrainer_source, RLConfig_source):
if "reward_funcs" not in RLTrainer_source:
return ""
# For new TRL we have /mean and /std
use_mean = "rewards/{reward_func_name}/mean" in RLTrainer_source
use_std = "rewards/{reward_func_name}/std" in RLTrainer_source
if not use_mean:
use_normal = "rewards/{reward_func_name}" in RLTrainer_source
else:
use_normal = False
log_metrics = (
"if not isinstance(reward_funcs, list): _reward_funcs = [reward_funcs]\n"
"else: _reward_funcs = reward_funcs\n"
"for reward_func in _reward_funcs:\n"
" try:\n"
" reward_func_name = reward_func.__name__\n"
f" if {use_mean}:\n"
" other_metrics.append(f'rewards/{reward_func_name}/mean')\n"
f" if {use_std}:\n"
" other_metrics.append(f'rewards/{reward_func_name}/std')\n"
f" if {use_normal}:\n"
" other_metrics.append(f'rewards/{reward_func_name}')\n"
" except: pass\n"
)
return log_metrics
RL_METRICS_CHANGES["grpo_trainer"].append(grpo_trainer_metrics)
def openenv_vllm_reload_weights():
# This function patches the trl openenv generate_rollout_completions function to:
# 1. Guard the reload_weights call (skip when sharing weights with vLLM)
# 2. Fix wake_up call to be compatible with unsloth (remove tags to wake everything)
#
# The issue: TRL's wake_up(tags=["kv_cache"]) only wakes kv_cache, leaving is_sleeping=True
# at the executor level. This causes unsloth's patched generate to try waking up again,
# resulting in double create_and_map on already-mapped handles.
#
# The fix: Use wake_up() with no tags, which wakes everything. Unsloth's patched
# CuMemAllocator.wake_up skips weights anyway, so this is safe.
if importlib.util.find_spec("trl") is None:
return
if Version(importlib_version("trl")) < Version("0.26.0"):
return
try:
import trl.experimental.openenv.utils as openenv_utils
import trl.experimental.openenv as openenv
except (ImportError, NameError, Exception) as e:
logger.info(f"Unsloth: Failed to import trl openenv: {e}")
logger.info(
"Unsloth: trl.experimental.openenv not available — skipping RL openenv patches."
)
return
# trl 0.28 changed the function name yet again! Thanks trl :)
patch_target_name = "_generate_rollout_completions_colocate"
if hasattr(openenv_utils, patch_target_name):
patch_target = getattr(openenv_utils, patch_target_name)
else:
# Older TRL versions may keep sleep/wake logic in the public dispatcher.
patch_target_name = "generate_rollout_completions"
patch_target = getattr(openenv_utils, patch_target_name)
# TRL 0.29.1+ ships some openenv helpers as compiled bytecode without
# accessible source on disk; inspect.getsource raises OSError("could
# not get source code") in that case. Skip the source-rewrite patch
# rather than crash. The unmodified TRL openenv path will run, which
# means the duplicate `collective_rpc("reload_weights")` is NOT
# stripped (line 1800 below) and `wake_up(tags=["kv_cache"])` is NOT
# retagged to `wake_up()` (line 1804). Users who do not use openenv
# GRPO are unaffected; openenv GRPO users on this TRL build may see
# redundant reload_weights calls or partial wake_up behavior.
try:
src = inspect.getsource(patch_target)
except OSError as e:
logger.warning(
f"Unsloth: Could not retrieve source for trl openenv "
f"{patch_target_name} ({e}); skipping rewrite. The unmodified "
f"TRL openenv path will run, so the duplicate reload_weights "
f"strip and the wake_up tag rewrite are NOT applied. Open an "
f"issue if you see redundant reload_weights or partial wake_up "
f"on openenv GRPO with this TRL build."
)
return
src = textwrap.dedent(src)
original_src = src
reload_weights_pattern = re.compile(
r"^(?P<indent>[ \t]*)(?P<obj>\S+)\.collective_rpc\(\s*(['\"])reload_weights\3\s*\)\s*$",
re.MULTILINE,
)
def replace_reload_weights(match):
indent = match.group("indent")
obj = match.group("obj")
return (
f"{indent}if not getattr({obj}, 'shared_weights', False):\n"
f'{indent} {obj}.collective_rpc("reload_weights")\n'
)
src = reload_weights_pattern.sub(replace_reload_weights, src)
# Change wake_up(tags=["kv_cache"]) to wake_up() - wake everything to set is_sleeping=False
# This prevents double wake_up issues. Unsloth's allocator skips weights anyway.
src = re.sub(r"\.wake_up\(tags=\[.*?\]\)", ".wake_up()", src)
if original_src == src:
logger.warning("Unsloth: Warning - regex did not match, patch may have failed")
return
# Execute and explicitly assign to module
local_ns = {}
exec(compile(src, "<unsloth>", "exec"), openenv_utils.__dict__, local_ns)
patched_func = local_ns[patch_target_name]
# Patch the target function in utils; if dispatcher was patched also update parent module alias.
setattr(openenv_utils, patch_target_name, patched_func)
if patch_target_name == "generate_rollout_completions":
openenv.generate_rollout_completions = patched_func
logger.info(f"Unsloth: Patched trl openenv {patch_target_name}")
RL_ADDITIONAL_FUNCTIONS["openenv"].append(openenv_vllm_reload_weights)
def vllm_generation_init_patch():
# trl moved vllm stuff to trl/generation/vllm_generation.py
# We need to patch it to not instantiate another vLLM instance if we already have one with fast_inference
# Edit the TRL source directly and install the patched function in the TRL module.
# https://github.com/huggingface/trl/commit/0eb66d8f2fc63b3d00d8dbc18f99c3f48750bd16
# This exists in trl versions 0.28.0 and above
if importlib.util.find_spec("trl") is None:
return
if Version(importlib_version("trl")) < Version("0.28.0"):
return
try:
import trl.generation.vllm_generation as vllm_generation
except (ImportError, NameError, Exception) as e:
logger.info(f"Unsloth: Failed to import trl.generation.vllm_generation: {e}")
return
def patch_vllm_generation_method(method_name, transform, marker, filename_suffix):
method = getattr(vllm_generation.VLLMGeneration, method_name, None)
if method is None:
logger.info(f"Unsloth: Could not find VLLMGeneration.{method_name}")
return False
try:
src = inspect.getsource(method)
except Exception as e:
logger.info(f"Unsloth: Could not get source of VLLMGeneration.{method_name}: {e}")
return False
src = textwrap.dedent(src)
if marker in src:
return True
src = transform(src)
filename = f"<unsloth_trl_vllm_generation_{filename_suffix}_patch>"
source_lines = [line + "\n" for line in src.splitlines()]
linecache.cache[filename] = (
len(src),
None,
source_lines,
filename,
)
local_ns = {}
exec(compile(src, filename, "exec"), vllm_generation.__dict__, local_ns)
setattr(vllm_generation.VLLMGeneration, method_name, local_ns[method_name])
return True
# Patch init to remove vLLM.LLM instantiation
def patch_init_vllm(src):
pattern = re.compile(
r"(?P<llm_block>^(?P<indent>[ \t]*)self\.llm\s*=\s*LLM\s*\(\n(?:.*\n)*?^(?P=indent)\))",
re.MULTILINE,
)
def replace_llm_block(match):
indent = match.group("indent")
llm_block = textwrap.dedent(match.group("llm_block"))
return (
f"{indent}if hasattr(model, 'vllm_engine'):\n"
f"{indent} # Unsloth already inits vLLM in fast inference mode. Do not redo :)\n"
f"{indent} self.llm = model.vllm_engine\n"
f"{indent} self.unsloth_fast_inference_lora = getattr(self.llm, 'shared_weights', False)\n"
f"{indent} if getattr(self.llm, 'shared_weights', False) and hasattr(model, 'load_lora'):\n"
f"{indent} self._unsloth_load_lora = model.load_lora\n"
f"{indent}else:\n" + textwrap.indent(llm_block, indent + " ")
)
patched_src, num_replacements = pattern.subn(replace_llm_block, src, count = 1)
if num_replacements == 0:
raise RuntimeError(
"Unsloth: Warning - regex did not match, VLLMGeneration._init_vllm patch may have failed"
)
return patched_src
# has some sync_weights or reload rpc calls.
# we patched the grpo_trainer to strip them for prev versions
# Ref: grpo_trainer__generate_single_turn above around L270-280
def patch_sync_weights(src):
pattern = re.compile(
r"^(?P<def_line>def sync_weights\(self\):\n)(?P<body>(?:.*\n)*)",
re.MULTILINE,
)
def replace_sync_weights(match):
body = match.group("body")
# Chain getattr so server mode (where self.llm is not set) does
# not raise AttributeError before the default kicks in.
guard = (
" if getattr(getattr(self, 'llm', None), 'shared_weights', False) or "
"getattr(self, 'unsloth_fast_inference_lora', False):\n"
" # Unsloth fast inference LoRA shares weights with vLLM already.\n"
" return\n\n"
)
return match.group("def_line") + guard + body
patched_src, num_replacements = pattern.subn(replace_sync_weights, src, count = 1)
if num_replacements == 0:
raise RuntimeError(
"Unsloth: Warning - regex did not match, VLLMGeneration.sync_weights patch may have failed"
)
return patched_src
def patch_generate(src):
pattern = re.compile(
r"^(?P<indent>[ \t]*)self\.llm\.collective_rpc\(\s*(['\"])reload_weights\2\s*\)\s*$",
re.MULTILINE,
)
def replace_reload_weights(match):
indent = match.group("indent")
# Chain getattr so server mode (no self.llm) is safe here too.
return (
f"{indent}if not (getattr(getattr(self, 'llm', None), 'shared_weights', False) or "
f"getattr(self, 'unsloth_fast_inference_lora', False)):\n"
f'{indent} self.llm.collective_rpc("reload_weights")'
)
patched_src, num_replacements = pattern.subn(replace_reload_weights, src, count = 1)
if num_replacements == 0:
raise RuntimeError(
"Unsloth: Warning - regex did not match, VLLMGeneration.generate patch may have failed"
)
# Inject lora_request when sharing weights (vLLM needs the adapter)
lora_generate_pattern = re.compile(
r"(self\.llm\.generate\([^\)]+)\)",
)
def inject_lora_request(match):
return (
f"{match.group(1)}, lora_request="
f"self._unsloth_load_lora('vllm_gen_lora', load_tensors=True) "
f"if hasattr(self, '_unsloth_load_lora') else None)"
)
patched_src = lora_generate_pattern.sub(inject_lora_request, patched_src)
return patched_src
try:
init_patched = patch_vllm_generation_method(
"_init_vllm",
patch_init_vllm,
"self.unsloth_fast_inference_lora = getattr(self.llm, 'shared_weights', False)",
"init_vllm",
)
sync_patched = patch_vllm_generation_method(
"sync_weights",
patch_sync_weights,
"if getattr(getattr(self, 'llm', None), 'shared_weights', False) or getattr(self, 'unsloth_fast_inference_lora', False):",
"sync_weights",
)
generate_patched = patch_vllm_generation_method(
"generate",
patch_generate,
"if not (getattr(getattr(self, 'llm', None), 'shared_weights', False) or getattr(self, 'unsloth_fast_inference_lora', False)):",
"generate",
)
except RuntimeError as e:
logger.warning(str(e))
return
if init_patched:
logger.info("Unsloth: Patched trl VLLMGeneration._init_vllm")
if sync_patched:
logger.info("Unsloth: Patched trl VLLMGeneration.sync_weights")
if generate_patched:
logger.info("Unsloth: Patched trl VLLMGeneration.generate")
RL_ADDITIONAL_FUNCTIONS["vllm_generation"].append(vllm_generation_init_patch)