e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
217 lines
8.7 KiB
Python
217 lines
8.7 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
|
|
|
|
"""TEMPLATE_TO_RESPONSES_MAPPER markers must match what the templates render.
|
|
|
|
The manual instruction/response markers are the fallback for
|
|
train_on_completions when auto-detection is unavailable, so a marker that
|
|
never matches the rendered chat template masks every assistant token and the
|
|
run dies on the all-labels-masked safety net. Six template families shipped
|
|
such markers:
|
|
|
|
mistral - "[INST] " / " [/INST]": the surrounding spaces fold into
|
|
the neighbouring tokens ("[INST]" is a single special
|
|
token in Mistral v0.3), so the padded strings never match.
|
|
llama - same space folding, plus llama-2 tokenizes [INST] after
|
|
<s> as bare "[" on transformers 5.x while the standalone
|
|
encoding gives "▁[", so the marker must anchor on <s>.
|
|
starling - trailing space after "GPT4 Correct Assistant:" folds
|
|
into the next content token ("▁Hello").
|
|
glm - "[gMASK]<sop>" renders once at text start, never before
|
|
later user turns; "<think>" is generation scaffolding
|
|
that non-final turns render as a lone "</think>".
|
|
qwen3-thinking - "<think>" is stripped from non-final assistant turns
|
|
(Qwen3-Thinking-2507) or never rendered (QwQ).
|
|
zephyr - role tags are plain text, and SentencePiece tokenizes
|
|
"<|assistant|>" differently at text start than after
|
|
"</s>\\n" mid-conversation; the markers need the leading
|
|
newline anchor to tokenize like a real turn boundary.
|
|
|
|
Literal assertions run everywhere; the token-level masking checks need the
|
|
representative tokenizers plus unsloth_zoo and skip when either is
|
|
unavailable (offline CI).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
_BACKEND_DIR = str(Path(__file__).resolve().parent.parent)
|
|
if _BACKEND_DIR not in sys.path:
|
|
sys.path.insert(0, _BACKEND_DIR)
|
|
|
|
# model_mappings is dependency-free: load it directly so these tests run
|
|
# without the studio venv / package import side effects.
|
|
_MM_PATH = Path(_BACKEND_DIR) / "utils" / "datasets" / "model_mappings.py"
|
|
_mm_spec = importlib.util.spec_from_file_location("_marker_test_mm", _MM_PATH)
|
|
model_mappings = importlib.util.module_from_spec(_mm_spec)
|
|
_mm_spec.loader.exec_module(model_mappings)
|
|
|
|
T2R = model_mappings.TEMPLATE_TO_RESPONSES_MAPPER
|
|
|
|
|
|
# ── Fixed entries: markers derived from what each representative tokenizer
|
|
# actually renders (see PR for the token-level derivation). ──
|
|
EXPECTED_FIXED = {
|
|
"mistral": {"instruction": "[INST]", "response": "[/INST]"},
|
|
"llama": {"instruction": "<s>[INST]", "response": "[/INST]"},
|
|
"starling": {"instruction": "GPT4 Correct User:", "response": "GPT4 Correct Assistant:"},
|
|
"glm": {"instruction": "<|user|>", "response": "<|assistant|>"},
|
|
"qwen3-thinking": {"instruction": "<|im_start|>user\n", "response": "<|im_start|>assistant\n"},
|
|
"zephyr": {"instruction": "\n<|user|>\n", "response": "\n<|assistant|>\n"},
|
|
}
|
|
|
|
# Spot-pin some known-good entries so a refactor cannot silently change them.
|
|
EXPECTED_UNCHANGED = {
|
|
"qwen3": {"instruction": "<|im_start|>user\n", "response": "<|im_start|>assistant\n"},
|
|
"llama-3.1": {
|
|
"instruction": "<|start_header_id|>user<|end_header_id|>\n\n",
|
|
"response": "<|start_header_id|>assistant<|end_header_id|>\n\n",
|
|
},
|
|
"phi-4": {
|
|
"instruction": "<|im_start|>user<|im_sep|>",
|
|
"response": "<|im_start|>assistant<|im_sep|>",
|
|
},
|
|
"gemma-3": {"instruction": "<start_of_turn>user\n", "response": "<start_of_turn>model\n"},
|
|
"gpt-oss": {
|
|
"instruction": "<|start|>user<|message|>",
|
|
"response": "<|start|>assistant<|channel|>final<|message|>",
|
|
},
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("template", sorted(EXPECTED_FIXED))
|
|
def test_fixed_marker_literals(template):
|
|
assert T2R[template] == EXPECTED_FIXED[template]
|
|
|
|
|
|
@pytest.mark.parametrize("template", sorted(EXPECTED_UNCHANGED))
|
|
def test_unchanged_marker_literals(template):
|
|
assert T2R[template] == EXPECTED_UNCHANGED[template]
|
|
|
|
|
|
def test_no_marker_is_empty_or_whitespace():
|
|
for template, parts in T2R.items():
|
|
assert parts["instruction"].strip(), template
|
|
assert parts["response"].strip(), template
|
|
|
|
|
|
# ── Token-level checks: markers must select exactly the assistant turns on a
|
|
# rendered two-turn fixture, and the final EOS label must never be -100. ──
|
|
|
|
REPRESENTATIVES = {
|
|
"mistral": ["unsloth/mistral-7b-instruct-v0.3"],
|
|
"llama": ["unsloth/llama-2-7b-chat"],
|
|
"starling": ["unsloth/Starling-LM-7B-beta"],
|
|
"glm": ["unsloth/GLM-4.7-Flash"],
|
|
"qwen3-thinking": ["unsloth/Qwen3-4B-Thinking-2507", "Qwen/QwQ-32B"],
|
|
"zephyr": ["unsloth/zephyr-sft"],
|
|
}
|
|
|
|
FIXTURE = [
|
|
{"role": "user", "content": "zebra alpha question one?"},
|
|
{"role": "assistant", "content": "grape reply number one."},
|
|
{"role": "user", "content": "zebra beta question two?"},
|
|
{"role": "assistant", "content": "grape reply number two."},
|
|
]
|
|
|
|
|
|
def _load_tokenizer(repo):
|
|
try:
|
|
from transformers import AutoTokenizer
|
|
except Exception as e: # pragma: no cover
|
|
pytest.skip(f"transformers unavailable: {e}")
|
|
try:
|
|
return AutoTokenizer.from_pretrained(repo)
|
|
except OSError as e:
|
|
pytest.skip(f"tokenizer {repo} unavailable (offline?): {e}")
|
|
except Exception:
|
|
# Tokenizer class newer than this transformers (e.g. GLM-4.7's
|
|
# TokenizersBackend): build directly from tokenizer.json.
|
|
try:
|
|
import json as _json
|
|
from huggingface_hub import hf_hub_download
|
|
from transformers import PreTrainedTokenizerFast
|
|
|
|
with open(hf_hub_download(repo, "tokenizer_config.json"), encoding = "utf-8") as f:
|
|
cfg = _json.load(f)
|
|
tok_file = hf_hub_download(repo, "tokenizer.json")
|
|
|
|
def _tokval(v):
|
|
return v["content"] if isinstance(v, dict) else v
|
|
|
|
return PreTrainedTokenizerFast(
|
|
tokenizer_file = tok_file,
|
|
chat_template = cfg.get("chat_template"),
|
|
**{
|
|
k: _tokval(cfg[k])
|
|
for k in ("bos_token", "eos_token", "pad_token", "unk_token")
|
|
if cfg.get(k) is not None
|
|
},
|
|
)
|
|
except Exception as e:
|
|
pytest.skip(f"tokenizer {repo} unavailable (offline?): {e}")
|
|
|
|
|
|
def _train_on_responses_only():
|
|
try:
|
|
from unsloth_zoo.dataset_utils import train_on_responses_only
|
|
except Exception as e:
|
|
pytest.skip(f"unsloth_zoo unavailable: {e}")
|
|
return train_on_responses_only
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"template,repo",
|
|
[(t, r) for t, repos in sorted(REPRESENTATIVES.items()) for r in repos],
|
|
)
|
|
def test_fixed_markers_token_level(template, repo):
|
|
tor = _train_on_responses_only()
|
|
tok = _load_tokenizer(repo)
|
|
parts = T2R[template]
|
|
|
|
msgs = [{"role": "system", "content": "You are a terse assistant."}] + FIXTURE
|
|
try:
|
|
ids = tok.apply_chat_template(msgs, tokenize = True, add_generation_prompt = False)
|
|
if hasattr(ids, "keys"):
|
|
ids = ids["input_ids"] # transformers 5.x returns a BatchEncoding
|
|
except Exception:
|
|
ids = tok.apply_chat_template(FIXTURE, tokenize = True, add_generation_prompt = False)
|
|
if hasattr(ids, "keys"):
|
|
ids = ids["input_ids"]
|
|
|
|
fn = tor(
|
|
None,
|
|
instruction_part = parts["instruction"],
|
|
response_part = parts["response"],
|
|
tokenizer = tok,
|
|
return_function = True,
|
|
)
|
|
labels = fn({"input_ids": [list(ids)]})["labels"][0]
|
|
|
|
n = len(ids)
|
|
trained = tok.decode([ids[i] for i in range(n) if labels[i] != -100])
|
|
masked = tok.decode([ids[i] for i in range(n) if labels[i] == -100])
|
|
|
|
# User and system content fully masked
|
|
assert "question one" not in trained and "question one" in masked
|
|
assert "question two" not in trained and "question two" in masked
|
|
assert "terse assistant" not in trained
|
|
# EVERY assistant turn trained, not just the last
|
|
assert "reply number one" in trained
|
|
assert "reply number two" in trained
|
|
# The final EOS (last non-whitespace token) must never be -100, or the
|
|
# fine-tuned model never learns to stop generating.
|
|
i = n - 1
|
|
while i > 0 and tok.decode([ids[i]]).strip() == "":
|
|
i -= 1
|
|
assert labels[i] != -100, f"final token {tok.convert_ids_to_tokens(int(ids[i]))!r} is masked"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(pytest.main([__file__, "-v"]))
|