* Auto-detect completion masking markers with template table fallback Studio's train_on_completions previously relied only on the hardcoded MODEL_TO_TEMPLATE_MAPPER / TEMPLATE_TO_RESPONSES_MAPPER tables and silently disabled masking when a model was not in the table, so unmapped models (LFM2-8B-A1B, DeepSeek, and others) trained on full sequences without telling the user. Several mapped templates (glm, mistral, llama, starling, zephyr, qwen3-thinking) also carried markers that mask every assistant token, which made every row drop in the post-masking filter. Both training callsites (CUDA trainer.py and MLX worker.py) now share utils.datasets.completion_masking.apply_completion_masking: - Try unsloth_zoo chat template auto-detection first; it raises loudly when the template cannot be parsed and never masks the EOS token. - gpt-oss models keep their manual markers so non-final assistant <|end|> tokens stay trained, matching current behavior. - If auto-detection raises, fall back to the template table exactly as before. - If the table also misses, emit an explicit user-visible warning that completion masking could not be applied and full-sequence training will occur, instead of a quiet log line. The >30 percent dropped-rows safety net in trainer.py now guards the auto path as well. Table consumers for inference and chat templates are unchanged. Validated against one representative tokenizer for every template in TEMPLATE_TO_RESPONSES_MAPPER plus the unmapped models: no template regresses; unit tests cover the four decision paths. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Restrict masking fallback to marker detection failures The auto branch wrapped the whole train_on_responses_only call, so a real failure while applying the masking (dataset map, tokenization) was treated as a detection miss and training silently proceeded on full sequences. Detect markers separately via get_chat_template_parts (test seam via detect_fn), then apply them with errors propagating, matching the manual path. Tokenizers with preset unsloth marker attrs skip detection and call bare so zoo reuses the stored parts. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fail the run when applying completion masking raises The helper already falls back internally on detection failures and returns applied=False on a double miss, so an exception reaching the callsites is a real failure applying the masking. Remove the callsite catches that downgraded it to full-sequence training; the run now fails visibly instead. Also use the explicit re-export alias form in utils/datasets/__init__.py for the two new names, satisfying the import-hoist source lint. * Import completion masking from its submodule The import-hoist source lint counts only real name loads, so package-level re-exports of the two new names cannot satisfy it. Import apply_completion_masking from utils.datasets.completion_masking directly at both callsites and leave utils/datasets/__init__.py untouched. * Completion masking: gpt-oss renames and MLX raw/alpaca parity Renamed or private gpt-oss checkpoints are name-detected as gpt-oss but miss the exact-name table; default them to the gpt-oss template markers instead of falling through to full-sequence training. Gate the MLX masking call on not raw_text_mode and format_type != alpaca, mirroring the CUDA path: raw/CPT text has no chat turns to mask and Alpaca-rendered text lacks the tokenizer's chat markers. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Define raw_text_mode outside the MLX feature-detect block With an older zoo lacking the append_eos config field, the masking gate referenced raw_text_mode before assignment. Hoist the assignment above the feature detection so both consumers see it. * Gate MLX masking on the formatter's resolved format format_type auto can resolve to alpaca or raw text; the masking skip checked only the requested value, so auto-detected Alpaca data got chat-template markers applied to rendered prompt text. Track the final_format returned by format_and_template_dataset and gate on it, matching the CUDA path. * Unwrap the mlx-lm TokenizerWrapper before marker checks The wrapper delegates plain reads to the wrapped HF tokenizer but hides underscore attrs, so preset unsloth markers were invisible and detection relied on the loader's call patch. Unwrap to the real tokenizer first, as the zoo MLX resolver does. * Tighten masking comments * gpt-oss: auto-detect markers first like every other template The quantized and BF16 gpt-oss checkpoints ship a chat template without the channel final header, so the pinned manual markers match nothing there and masking trained zero tokens. Auto-detection derives markers from whichever template the checkpoint ships and keeps the final terminator trained; the manual gpt-oss markers remain the detection failure fallback, including for renamed checkpoints. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Tighten comments --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
144 lines
5.9 KiB
Python
144 lines
5.9 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Completion-only masking policy shared by the CUDA and MLX training paths.
|
|
|
|
Decides how train_on_responses_only is applied for a model: chat template
|
|
auto-detection first, manual TEMPLATE_TO_RESPONSES_MAPPER markers as the
|
|
fallback. gpt-oss included: its quantized checkpoints ship a different
|
|
chat template, so only detection from the actual template is reliable.
|
|
"""
|
|
|
|
from .model_mappings import (
|
|
MODEL_TO_TEMPLATE_MAPPER,
|
|
TEMPLATE_TO_RESPONSES_MAPPER,
|
|
is_gpt_oss_model_name,
|
|
)
|
|
|
|
|
|
def lookup_manual_markers(model_name):
|
|
"""Return (template_name, instruction_part, response_part) from the
|
|
manual template table, with None parts when the model or template is
|
|
not mapped."""
|
|
template = MODEL_TO_TEMPLATE_MAPPER.get((model_name or "").lower())
|
|
markers = TEMPLATE_TO_RESPONSES_MAPPER.get(template) if template else None
|
|
if markers:
|
|
return template, markers["instruction"], markers["response"]
|
|
return template, None, None
|
|
|
|
|
|
def apply_completion_masking(
|
|
trainer,
|
|
model_name,
|
|
train_fn,
|
|
num_proc = None,
|
|
notify = None,
|
|
detect_fn = None,
|
|
):
|
|
"""Apply completion-only masking with auto-detection first and the manual
|
|
template table as fallback.
|
|
|
|
Args:
|
|
trainer: The platform trainer (SFTTrainer or MLXTrainer).
|
|
model_name: Model repo id used for table lookup and the gpt-oss
|
|
renamed-checkpoint fallback.
|
|
train_fn: The platform train_on_responses_only callable.
|
|
num_proc: Forwarded to train_fn when not None (CUDA path only).
|
|
notify: Optional callback notify(level, message) with level "info" or
|
|
"warning" for user-visible progress and warnings.
|
|
detect_fn: Marker detector (tokenizer/processor) -> (instruction_part,
|
|
response_part). Defaults to unsloth_zoo's get_chat_template_parts,
|
|
which raises loudly when the template cannot be parsed. Test seam.
|
|
|
|
Returns:
|
|
(trainer, applied): the possibly wrapped trainer and whether masking
|
|
was applied. When applied is False the trainer is unchanged and
|
|
training runs on full sequences.
|
|
|
|
Only marker DETECTION failures trigger the table fallback. Exceptions
|
|
raised while applying the masking (dataset map, tokenization) propagate
|
|
to the caller in both the auto and manual paths, so a real failure stops
|
|
the run instead of silently changing the training objective.
|
|
"""
|
|
if notify is None:
|
|
notify = lambda level, message: None
|
|
kwargs = {}
|
|
if num_proc is not None:
|
|
kwargs["num_proc"] = num_proc
|
|
|
|
template, instruction_part, response_part = lookup_manual_markers(model_name)
|
|
|
|
# gpt-oss goes auto-first: quantized/BF16 checkpoints ship a channel-less
|
|
# template, so the manual markers match nothing (zero tokens trained). Auto
|
|
# derives markers from whichever template ships, and per the harmony format
|
|
# only the final terminator carries stop supervision. Renamed checkpoints
|
|
# miss the exact-name table, so give the fallback the gpt-oss markers.
|
|
if is_gpt_oss_model_name(model_name) and not (instruction_part and response_part):
|
|
markers = TEMPLATE_TO_RESPONSES_MAPPER.get("gpt-oss")
|
|
if markers:
|
|
template = "gpt-oss"
|
|
instruction_part = markers["instruction"]
|
|
response_part = markers["response"]
|
|
processor = getattr(trainer, "processing_class", None) or getattr(trainer, "tokenizer", None)
|
|
# mlx-lm TokenizerWrapper hides underscore attrs, so preset _unsloth_*
|
|
# markers are invisible through it. Unwrap to the real tokenizer (as
|
|
# zoo's MLX resolver does) before the preset check and detection.
|
|
if type(processor).__name__ == "TokenizerWrapper":
|
|
wrapped = getattr(processor, "_tokenizer", None)
|
|
if wrapped is not None:
|
|
processor = wrapped
|
|
inner = getattr(processor, "tokenizer", processor)
|
|
if hasattr(inner, "_unsloth_input_part") and hasattr(inner, "_unsloth_output_part"):
|
|
# Markers preset on the tokenizer; zoo reuses them on a bare call.
|
|
trainer = train_fn(trainer, **kwargs)
|
|
notify(
|
|
"info",
|
|
"Train on responses only configured via tokenizer preset markers",
|
|
)
|
|
return trainer, True
|
|
auto_instruction = auto_response = None
|
|
try:
|
|
if detect_fn is None:
|
|
# Torch-backed import is fine: the MLX train_fn itself requires
|
|
# unsloth_zoo.dataset_utils, so a torch-free host cannot mask either way.
|
|
from unsloth_zoo.dataset_utils import get_chat_template_parts as detect_fn
|
|
auto_instruction, auto_response = detect_fn(processor)
|
|
except Exception as e:
|
|
notify(
|
|
"warning",
|
|
f"Auto-detection of instruction/response markers failed ({e}); "
|
|
f"falling back to the template table",
|
|
)
|
|
if auto_instruction and auto_response:
|
|
trainer = train_fn(
|
|
trainer,
|
|
instruction_part = auto_instruction,
|
|
response_part = auto_response,
|
|
**kwargs,
|
|
)
|
|
notify(
|
|
"info",
|
|
"Train on responses only configured via chat template auto-detection",
|
|
)
|
|
return trainer, True
|
|
|
|
if instruction_part and response_part:
|
|
trainer = train_fn(
|
|
trainer,
|
|
instruction_part = instruction_part,
|
|
response_part = response_part,
|
|
**kwargs,
|
|
)
|
|
notify(
|
|
"info",
|
|
f"Train on responses only configured with template table markers ({template})",
|
|
)
|
|
return trainer, True
|
|
|
|
notify(
|
|
"warning",
|
|
f"'Train on completions' could not be applied for {model_name}: no "
|
|
f"auto-detected or mapped instruction/response markers. Training "
|
|
f"will run on full sequences (prompts included).",
|
|
)
|
|
return trainer, False
|