Fix text-only VLM CPT packing truncation (#7211)

* 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

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

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

* Install wrapped-packing setup at the signature, not the Zoo license comment

The _unsloth_wrapped_packing / _inspect setup block was injected by matching the
exact 'All Unsloth Zoo code licensed under LGPLv3' comment line in the sourced
sft_prepare_dataset. The unsloth_zoo dependency is only lower-bounded, so a newer
Zoo that moves or drops that header made the setup a silent no-op while the
truncation and pack_dataset rewrites still emitted references to those names,
raising NameError on every SFT dataset preparation.

Anchor the setup on the function signature instead (a structural location that
always exists) and fail loudly if it cannot be found, so the helper variables are
always defined before they are referenced across Zoo versions.

Adds a regression test that patches in a Zoo source without the license header.

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

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

---------

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>
Co-authored-by: danielhanchen <danielhanchen@gmail.com>
This commit is contained in:
alkinun 2026-07-20 10:23:37 +03:00 committed by GitHub
commit 9e334d552c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 610 additions and 38 deletions

View file

@ -3425,15 +3425,19 @@ class UnslothTrainer:
logger.info(
f"CPT: using UnslothTrainer with embedding_learning_rate={embedding_lr}\n"
)
cpt_args = _UnslothTrainingArguments(
embedding_learning_rate = embedding_lr,
**config_args,
)
if config_args.get("packing", False):
cpt_args.packing_strategy = "wrapped"
logger.info("CPT packing strategy: wrapped\n")
trainer_kwargs = {
"model": self.model,
"tokenizer": sft_tokenizer,
"train_dataset": dataset["dataset"],
"data_collator": data_collator,
"args": _UnslothTrainingArguments(
embedding_learning_rate = embedding_lr,
**config_args,
),
"args": cpt_args,
}
if eval_dataset is not None:
trainer_kwargs["eval_dataset"] = eval_dataset

View file

@ -14,6 +14,7 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from unsloth import FastLanguageModel
import unsloth.trainer as trainer_module
from unsloth.utils import attention_dispatch as attention_dispatch_utils
from unsloth.utils.packing import (
configure_padding_free,
@ -29,7 +30,7 @@ from unittest.mock import patch
import pytest
import torch
from datasets import Dataset
from datasets import Dataset, IterableDataset
from trl import SFTConfig, SFTTrainer
from trl.trainer.sft_trainer import DataCollatorForLanguageModeling
@ -160,6 +161,374 @@ def test_configure_padding_free():
assert config.remove_unused_columns is False
def _patch_fake_sft_trainer():
class FakeSFTTrainer:
def __init__(self, *args, **kwargs):
self.model = args[0] if len(args) >= 1 else kwargs["model"]
self.args = args[1] if len(args) >= 2 else kwargs["args"]
self.data_collator = args[2] if len(args) >= 3 else kwargs.get("data_collator")
trainer_module._patch_sft_trainer_auto_packing(SimpleNamespace(SFTTrainer = FakeSFTTrainer))
return FakeSFTTrainer
def _vlm_model():
return SimpleNamespace(
config = SimpleNamespace(
architectures = ["Gemma4ForConditionalGeneration"],
model_type = "gemma4",
vision_config = SimpleNamespace(),
),
max_seq_length = 16,
)
def _text_model():
return SimpleNamespace(
config = SimpleNamespace(
architectures = ["LlamaForCausalLM"],
model_type = "llama",
),
max_seq_length = 16,
)
class _CharacterTokenizer:
bos_token = None
eos_token = None
chat_template = None
def __call__(self, texts, **kwargs):
is_batched = isinstance(texts, list)
if not is_batched:
texts = [texts]
input_ids = [[ord(char) for char in text] for text in texts]
if kwargs.get("truncation") and kwargs.get("max_length") is not None:
input_ids = [ids[: kwargs["max_length"]] for ids in input_ids]
return {"input_ids": input_ids if is_batched else input_ids[0]}
def test_vlm_text_dataset_allows_explicit_packing():
fake_trainer = _patch_fake_sft_trainer()
config = SimpleNamespace(packing = True, padding_free = None, remove_unused_columns = True)
trainer = fake_trainer(
model = _vlm_model(),
args = config,
processing_class = object(),
train_dataset = Dataset.from_dict({"text": ["text-only CPT sample"]}),
)
assert config.packing is True
assert config.padding_free is True
assert trainer.model._unsloth_allow_packed_overlength is True
def test_vlm_without_processing_class_still_disables_packing():
fake_trainer = _patch_fake_sft_trainer()
config = SimpleNamespace(packing = True, padding_free = None, remove_unused_columns = True)
fake_trainer(
_vlm_model(),
config,
None,
Dataset.from_dict({"text": ["text-only sample"]}),
)
assert config.packing is False
assert config.padding_free is False
@pytest.mark.parametrize(
("model_type", "architecture"),
(
("t5", "T5ForConditionalGeneration"),
("bart", "BartForConditionalGeneration"),
("whisper", "WhisperForConditionalGeneration"),
("csm", "CsmForConditionalGeneration"),
),
)
def test_nonvision_conditional_generation_keeps_packing(model_type, architecture):
fake_trainer = _patch_fake_sft_trainer()
config = SimpleNamespace(packing = True, padding_free = None, remove_unused_columns = True)
model = SimpleNamespace(
config = SimpleNamespace(model_type = model_type, architectures = [architecture]),
max_seq_length = 16,
)
trainer = fake_trainer(
model,
config,
None,
Dataset.from_dict({"text": ["text-only sample"]}),
)
assert config.packing is True
assert config.padding_free is True
assert trainer.model._unsloth_allow_packed_overlength is True
def test_vlm_vision_dataset_still_disables_packing():
fake_trainer = _patch_fake_sft_trainer()
config = SimpleNamespace(packing = True, padding_free = None, remove_unused_columns = True)
fake_trainer(
_vlm_model(),
config,
None,
Dataset.from_dict({"images": [None], "text": ["multimodal sample"]}),
None,
object(),
)
assert config.packing is False
assert config.padding_free is False
@pytest.mark.parametrize(
"vision_column",
("pixel_values", "pixel_attention_mask", "image_grid_thw"),
)
def test_vlm_preprocessed_vision_dataset_disables_packing(vision_column):
fake_trainer = _patch_fake_sft_trainer()
config = SimpleNamespace(packing = True, padding_free = None, remove_unused_columns = True)
fake_trainer(
model = _vlm_model(),
args = config,
processing_class = object(),
train_dataset = Dataset.from_dict({"input_ids": [[1]], vision_column: [None]}),
)
assert config.packing is False
assert config.padding_free is False
@pytest.mark.parametrize("dict_eval", (False, True))
def test_vlm_vision_eval_dataset_disables_packing(dict_eval):
fake_trainer = _patch_fake_sft_trainer()
config = SimpleNamespace(packing = True, padding_free = None, remove_unused_columns = True)
eval_dataset = Dataset.from_dict({"input_ids": [[1]], "pixel_values": [None]})
if dict_eval:
eval_dataset = {"vision": eval_dataset}
fake_trainer(
model = _vlm_model(),
args = config,
processing_class = object(),
train_dataset = Dataset.from_dict({"text": ["text-only training sample"]}),
eval_dataset = eval_dataset,
)
assert config.packing is False
assert config.padding_free is False
def test_vlm_streaming_vision_dataset_without_metadata_disables_packing():
fake_trainer = _patch_fake_sft_trainer()
config = SimpleNamespace(packing = True, padding_free = None, remove_unused_columns = True)
dataset = IterableDataset.from_generator(
lambda: iter([{"images": [None], "text": "multimodal sample"}])
)
assert dataset.column_names is None
fake_trainer(
model = _vlm_model(),
args = config,
processing_class = object(),
train_dataset = dataset,
)
assert config.packing is False
assert config.padding_free is False
assert next(iter(dataset))["text"] == "multimodal sample"
@pytest.mark.parametrize("data_collator", (None, object()))
def test_stateful_stream_is_not_consumed_during_detection(data_collator):
class StatefulDataset:
def __init__(self):
self.rows = iter([{"text": "first"}, {"text": "second"}])
def __iter__(self):
return (row for row in self.rows)
fake_trainer = _patch_fake_sft_trainer()
config = SimpleNamespace(packing = True, padding_free = None, remove_unused_columns = True)
dataset = StatefulDataset()
fake_trainer(
model = _vlm_model(),
args = config,
processing_class = object(),
data_collator = data_collator,
train_dataset = dataset,
)
assert config.packing is False
assert config.padding_free is False
assert next(iter(dataset))["text"] == "first"
def test_text_model_stream_without_metadata_keeps_packing():
class StatefulDataset:
def __init__(self):
self.rows = iter([{"text": "first"}, {"text": "second"}])
def __iter__(self):
return (row for row in self.rows)
fake_trainer = _patch_fake_sft_trainer()
config = SimpleNamespace(packing = True, padding_free = None, remove_unused_columns = True)
dataset = StatefulDataset()
trainer = fake_trainer(
model = _text_model(),
args = config,
processing_class = object(),
train_dataset = dataset,
)
assert config.packing is True
assert config.padding_free is True
assert trainer.model._unsloth_allow_packed_overlength is True
assert next(iter(dataset))["text"] == "first"
def test_bfd_packing_truncates_before_packing(monkeypatch):
args = SimpleNamespace(
dataset_num_proc = 1,
dataset_text_field = "text",
max_length = 4,
packing_strategy = "bfd",
)
trainer = SimpleNamespace(model = None)
dataset = Dataset.from_dict({"prompt": ["abc"], "completion": ["defghij"]})
prepare_globals = SFTTrainer._prepare_dataset.__globals__
def passthrough_pack_dataset(dataset, seq_length, strategy, map_kwargs):
return dataset
monkeypatch.setitem(prepare_globals, "pack_dataset", passthrough_pack_dataset)
packed = SFTTrainer._prepare_dataset(
trainer,
dataset,
_CharacterTokenizer(),
args,
True,
None,
"train",
)
assert len(packed["input_ids"][0]) == args.max_length
def test_wrapped_strategy_without_packing_still_truncates():
args = SimpleNamespace(
dataset_num_proc = 1,
dataset_text_field = "text",
max_length = 4,
packing_strategy = "wrapped",
)
trainer = SimpleNamespace(model = None)
dataset = Dataset.from_dict({"text": ["abcdefghi"]})
prepared = SFTTrainer._prepare_dataset(
trainer,
dataset,
_CharacterTokenizer(),
args,
False,
None,
"train",
)
assert len(prepared["input_ids"][0]) == args.max_length
@pytest.mark.parametrize("legacy_api", (False, True))
def test_wrapped_packing_preserves_overlength_tokens(monkeypatch, legacy_api):
args_kwargs = {
"dataset_num_proc": 1,
"dataset_text_field": "text",
"max_length": 4,
}
if not legacy_api:
args_kwargs["packing_strategy"] = "wrapped"
args = SimpleNamespace(**args_kwargs)
trainer = SimpleNamespace(model = None)
dataset = Dataset.from_dict({"text": ["abcdefghi"]})
prepare_globals = SFTTrainer._prepare_dataset.__globals__
pack_dataset = prepare_globals["pack_dataset"]
def legacy_pack_dataset(
dataset,
seq_length,
map_kwargs = None,
):
return pack_dataset(dataset, seq_length, "wrapped", map_kwargs)
if legacy_api:
monkeypatch.setitem(prepare_globals, "pack_dataset", legacy_pack_dataset)
packed = SFTTrainer._prepare_dataset(
trainer,
dataset,
_CharacterTokenizer(),
args,
True,
None,
"train",
)
packed_ids = packed["input_ids"]
assert sum(len(input_ids) for input_ids in packed_ids) == 9
assert all(len(input_ids) <= args.max_length for input_ids in packed_ids)
# Named to match the unsloth_zoo helper: sft_trainer_prepare_dataset sources it by
# name and renames "def sft_prepare_dataset" -> "def _prepare_dataset". This fixture
# deliberately omits the "All Unsloth Zoo code licensed under LGPLv3" header to emulate
# a newer, compatible Zoo whose header moved (the dependency is only lower-bounded).
def sft_prepare_dataset(
self, dataset, processing_class, args, packing, formatting_func, dataset_text_field
):
do_truncation = True
# Mirror the Zoo call so the "truncation = do_truncation," injection anchor
# survives formatting (a bare tuple assignment gets rewritten to a paren form).
dataset = processing_class(
dataset,
truncation = do_truncation,
)
return dataset
def test_wrapped_packing_setup_survives_missing_zoo_header(monkeypatch):
# Regression: the wrapped-packing setup used to anchor on the Zoo license comment,
# so a header change made it a no-op while the truncation reference still landed,
# NameError-ing every SFT dataset preparation. It must now install via the
# signature and always precede the reference.
import ast
import textwrap
import unsloth.models.rl_replacements as rlr
monkeypatch.setitem(rlr.RL_REPLACEMENTS, "sft_prepare_dataset", sft_prepare_dataset)
source = (
"def _prepare_dataset(self, dataset, processing_class, args, packing, "
"formatting_func, dataset_text_field):\n return dataset\n"
)
patched = rlr.sft_trainer_prepare_dataset("_prepare_dataset", source)
assert "_unsloth_wrapped_packing = packing" in patched
assert "import inspect as _inspect" in patched
assert "not _unsloth_wrapped_packing" in patched
assert patched.index("_unsloth_wrapped_packing = packing") < patched.index(
"truncation = do_truncation and not _unsloth_wrapped_packing"
)
ast.parse(textwrap.dedent(patched))
class _DummyChild(torch.nn.Module):
def __init__(self):
super().__init__()

View file

@ -276,17 +276,13 @@ def dpo_trainer_vision_signature_columns(function_name, 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'
f"{_extra_columns}"
f' "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'
f"{_extra_columns}"
f' "ref_chosen_logps",\n',
f' "image_sizes",\n{_extra_columns} "ref_chosen_logps",\n',
)
@ -458,6 +454,60 @@ def sft_trainer_prepare_dataset(function_name, function):
if matched:
# Use fast version!
function = inspect.getsource(fast_sft_prepare_dataset)
# why: install the wrapped-packing setup (and the `_inspect` import the
# truncation / pack_dataset rewrites below depend on) at the function
# signature, a structural anchor that always exists, rather than the
# unsloth_zoo license-comment line. That header is only lower-bounded, so a
# newer Zoo may move or drop it; anchoring there let the setup silently
# no-op while the references still landed, NameError-ing every SFT dataset
# preparation. Fail loudly if even the signature cannot be located.
_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"
)
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."
)
function = function.replace(
"truncation = do_truncation,",
"truncation = do_truncation and not _unsloth_wrapped_packing,",
)
function = function.replace(
"if do_truncation and max_seq_length > 0:",
"if do_truncation and not _unsloth_wrapped_packing and max_seq_length > 0:",
)
function = function.replace(
"""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 "strategy" in _inspect.signature(pack_dataset).parameters:
_pack_kwargs["strategy"] = getattr(args, "packing_strategy", "bfd")
dataset = pack_dataset(
dataset.select_columns(used_column_names),
max_seq_length,
**_pack_kwargs,
)""",
)
function = function.split("\n")
function = "\n".join(" " * 4 + x for x in function)
function = function.replace("def sft_prepare_dataset", "def _prepare_dataset")
@ -2120,19 +2170,21 @@ def grpo_trainer_compute_loss(function_name, function):
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
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]
)
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(

View file

@ -100,6 +100,10 @@ PADDING_FREE_BLOCKLIST = {
"gemma2", # - gemma2: Uses slow_attention_softcapping which has torch.compile issues
"gpt_oss", # - gpt_oss: Uses Flex Attention which doesn't handle padding_free correctly
}
# Hybrid linear-attention / state-space models (Qwen3.5, Qwen3-Next, ...) carry a
# recurrent gated-delta state plus a causal conv1d. Sample packing / padding-free
# flattens the batch, so those ops leak state across sequence boundaries. Detected
# structurally by _is_hybrid_linear_attention_model rather than by model name.
def _should_pack(config) -> bool:
@ -137,6 +141,132 @@ def _should_skip_auto_packing_error(exc: Exception) -> bool:
return any(msg in message for msg in _AUTO_PACK_SKIP_MESSAGES)
_VISION_DATASET_KEYS = frozenset(
{
"image",
"images",
"image_grid_thw",
"image_position_ids",
"image_sizes",
"mm_token_type_ids",
"pixel_attention_mask",
"pixel_position_ids",
"pixel_values",
"pixel_values_videos",
"video",
"videos",
"video_grid_thw",
}
)
def _is_vlm_config(config, model_types = ()) -> bool:
if any(
hasattr(config, attr)
for attr in ("vision_config", "img_processor", "image_token_index", "projector_config")
):
return True
architectures = getattr(config, "architectures", None) or ()
try:
from transformers.models.auto import modeling_auto
mappings = (
getattr(modeling_auto, "MODEL_FOR_IMAGE_TEXT_TO_TEXT_MAPPING_NAMES", {}) or {},
getattr(modeling_auto, "MODEL_FOR_VISION_2_SEQ_MAPPING_NAMES", {}) or {},
)
registry_types = set().union(*(mapping.keys() for mapping in mappings))
registry_classes = set().union(*(mapping.values() for mapping in mappings))
config_types = set(model_types or ())
model_type = getattr(config, "model_type", None)
if model_type is not None:
config_types.add(model_type)
if not config_types.isdisjoint(registry_types) or any(
architecture in registry_classes for architecture in architectures
):
return True
except Exception:
pass
return any(
isinstance(architecture, str) and architecture.endswith("ForVisionText2Text")
for architecture in architectures
)
def _is_vision_dataset(dataset, *, unknown_is_vision = False) -> bool:
if dataset is None:
return False
column_names = getattr(dataset, "column_names", None)
if column_names is not None:
return not _VISION_DATASET_KEYS.isdisjoint(column_names)
# Unknown-schema streams cannot be safely probed without potentially dropping a sample.
return unknown_is_vision
def _is_vision_eval_dataset(dataset, *, unknown_is_vision = False) -> bool:
if isinstance(dataset, dict):
return any(
_is_vision_dataset(split, unknown_is_vision = unknown_is_vision)
for split in dataset.values()
)
return _is_vision_dataset(dataset, unknown_is_vision = unknown_is_vision)
_HYBRID_CONFIG_MARKERS = (
"linear_conv_kernel_dim",
"linear_key_head_dim",
"linear_value_head_dim",
"full_attention_interval",
)
def _is_hybrid_linear_attention_model(model) -> bool:
"""Detect models mixing linear-attention / state-space mixers (gated-delta,
Mamba-style) with a causal conv1d, e.g. Qwen3.5 / Qwen3-Next. Packing and
padding-free flatten the batch, and those recurrent + conv ops leak state
across sequence boundaries, so they must not be packed. Uses composite
structural evidence rather than a model-name match."""
if model is None:
return False
# Config-level: explicit hybrid layer schedule or linear-attn markers.
for config in (
getattr(model, "config", None),
getattr(getattr(model, "config", None), "text_config", None),
):
if config is None:
continue
layer_types = getattr(config, "layer_types", None)
if isinstance(layer_types, (list, tuple)) and any(
isinstance(t, str) and "linear_attention" in t for t in layer_types
):
return True
if any(hasattr(config, marker) for marker in _HYBRID_CONFIG_MARKERS):
return True
# Module-level: a mixer carrying a recurrent gated-delta op plus a conv1d.
named_modules = getattr(model, "named_modules", None)
if named_modules is None:
return False
seen = set()
for _, module in named_modules():
if id(module) in seen:
continue
seen.add(id(module))
cls = type(module).__name__
if not (
cls.endswith("GatedDeltaNet") or "LinearAttention" in cls or cls.endswith("Mamba2Mixer")
):
continue
has_recurrent = any(
hasattr(module, attr)
for attr in ("chunk_gated_delta_rule", "recurrent_gated_delta_rule", "A_log")
)
if has_recurrent and hasattr(module, "conv1d"):
return True
return False
# Unsloth gradient accumulation fix:
from transformers import __version__ as transformers_version, ProcessorMixin
@ -498,30 +628,43 @@ def _patch_sft_trainer_auto_packing(trl_module):
else:
config_arg = kwargs.get("args")
model = kwargs.get("model")
is_unsupported_model = False
model = args[0] if len(args) >= 1 else kwargs.get("model")
is_vlm = False
is_unsupported_model = False
is_hybrid = False
if model is not None:
model_config = getattr(model, "config", None)
if model_config is not None:
model_types = get_transformers_model_type(model_config)
is_unsupported_model = any(x in PADDING_FREE_BLOCKLIST for x in model_types)
is_vlm = _is_vlm_config(model_config, model_types)
is_hybrid = _is_hybrid_linear_attention_model(model)
architectures = getattr(model_config, "architectures", None)
if architectures is None:
architectures = []
is_vlm = any(x.endswith("ForConditionalGeneration") for x in architectures)
is_vlm = is_vlm or hasattr(model_config, "vision_config")
processing_class = kwargs.get("processing_class") or kwargs.get("tokenizer")
data_collator = kwargs.get("data_collator")
processing_class = (
args[5] if len(args) >= 6 else kwargs.get("processing_class") or kwargs.get("tokenizer")
)
data_collator = args[2] if len(args) >= 3 else kwargs.get("data_collator")
train_dataset = args[3] if len(args) >= 4 else kwargs.get("train_dataset")
eval_dataset = args[4] if len(args) >= 5 else kwargs.get("eval_dataset")
is_processor = isinstance(processing_class, ProcessorMixin)
is_auto_processor_vlm = is_vlm and processing_class is None
is_vision_dataset = (
data_collator is None
and not is_processor
and (
_is_vision_dataset(train_dataset, unknown_is_vision = is_vlm)
or _is_vision_eval_dataset(eval_dataset, unknown_is_vision = is_vlm)
)
)
# Disable padding-free for VLMs / custom collators / blocklisted models
blocked = (
(data_collator is not None)
or isinstance(processing_class, ProcessorMixin)
or is_vlm
or is_processor
or is_auto_processor_vlm
or is_vision_dataset
or is_unsupported_model
or is_hybrid
or (
os.environ.get("UNSLOTH_RETURN_LOGITS", "0") == "1"
) # Disable padding free on forced logits
@ -535,10 +678,14 @@ def _patch_sft_trainer_auto_packing(trl_module):
if blocked and requested_pack:
reason = "custom data collator"
if data_collator is None and isinstance(processing_class, ProcessorMixin):
if data_collator is None and is_processor:
reason = "processor-based model"
elif is_vlm:
reason = "vision-language model"
elif is_auto_processor_vlm:
reason = "vision-language model with auto processor"
elif is_vision_dataset:
reason = "vision dataset"
elif is_hybrid:
reason = "hybrid linear-attention model"
elif is_unsupported_model:
reason = f"unsupported model type(s): {', '.join(model_types)}"
message = f"Unsloth: Sample packing skipped ({reason} detected)."