unsloth/tests/_grpo_dispatch_source.py
Daniel Han 42ed15f4cc
Some checks are pending
Core / Core (HF=4.57.6 + TRL<1) (push) Waiting to run
Local Agent Guides CI / file-edit (claude) (push) Waiting to run
Local Agent Guides CI / file-edit (codex) (push) Waiting to run
Local Agent Guides CI / file-edit (hermes) (push) Waiting to run
Local Agent Guides CI / file-edit (openclaw) (push) Waiting to run
Security audit / advisory audit (pip + npm + cargo) (push) Waiting to run
Security audit / npm scan-packages (Unsloth frontend tarballs) (push) Waiting to run
Core / Core (HF=latest + TRL=latest) (push) Waiting to run
Core / llama.cpp build + smoke (push) Waiting to run
Core / Core (HF=default + TRL=default) (push) Waiting to run
Cross-platform parity / parity (ubuntu-latest) (push) Waiting to run
Cross-platform parity / parity (windows-latest) (push) Waiting to run
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Waiting to run
Local Agent Guides CI / connection (hermes) (push) Waiting to run
Local Agent Guides CI / connection (openclaw) (push) Waiting to run
Local Agent Guides CI / connection (opencode) (push) Waiting to run
Local Agent Guides CI / connection (pi) (push) Waiting to run
Local Agent Guides CI / file-edit (opencode) (push) Waiting to run
Local Agent Guides CI / file-edit (pi) (push) Waiting to run
Local Agent Guides CI / resume (claude) (push) Waiting to run
Local Agent Guides CI / resume (codex) (push) Waiting to run
Local Agent Guides CI / resume (opencode) (push) Waiting to run
Local Agent Guides CI / resume (pi) (push) Waiting to run
Local Agent Guides CI / prompt-cache (gemma-3-270m) (push) Waiting to run
Local Agent Guides CI / connection (claude) (push) Waiting to run
Local Agent Guides CI / connection (codex) (push) Waiting to run
MLX CI on Mac M1 / dispatch (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Security audit / pip scan-packages :: extras (push) Waiting to run
Security audit / pip scan-packages :: studio (push) Waiting to run
Security audit / pip scan-packages :: hf-stack (push) Waiting to run
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Waiting to run
Security audit / pytest tests/security (push) Waiting to run
Security audit / npm provenance + new install-script diff (push) Waiting to run
Unsloth API CI / Unsloth API & Auth Tests (push) Waiting to run
Backend CI / (Python 3.10) (push) Waiting to run
Backend CI / (Python 3.11) (push) Waiting to run
Backend CI / (Python 3.12) (push) Waiting to run
Backend CI / (Python 3.13) (push) Waiting to run
Backend CI / Repo tests (CPU) (push) Waiting to run
Unsloth export capability / capability (ubuntu-latest) (push) Waiting to run
Unsloth export capability / capability (windows-latest) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Waiting to run
Frontend CI / Frontend build + bundle sanity (push) Waiting to run
Unsloth GGUF CI / OpenAI, Anthropic API tests (push) Waiting to run
Unsloth GGUF CI / Tool calling Tests (push) Waiting to run
Unsloth GGUF CI / JSON, images (push) Waiting to run
Unsloth load-orchestrator CI / test (push) Waiting to run
Mac Studio GGUF CI / GGUF inference smoke (API, tools, vision) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Waiting to run
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Waiting to run
Unsloth Tauri CI / Rust unit tests (windows) (push) Waiting to run
Unsloth UI CI / Chat UI Tests (push) Waiting to run
Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Windows Unsloth API CI / Unsloth API & Auth Tests (push) Waiting to run
Windows Unsloth GGUF CI / GPU prebuilt resolves without Visual Studio (push) Waiting to run
Windows Unsloth GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2022) (push) Waiting to run
Windows Unsloth GGUF CI / real-VS detection (VS 2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Waiting to run
Windows Unsloth GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Waiting to run
Mac Studio UI + API + Update CI / Chat UI, API and Update Tests (push) Waiting to run
Unsloth Tauri CI / Tauri Linux debug build (no codesign) (push) Waiting to run
Windows Unsloth GGUF CI / GGUF inference smoke (API, tools, vision) (push) Waiting to run
Windows Unsloth GGUF CI / Unsloth install + inference without Visual Studio (push) Waiting to run
Windows Unsloth UI CI / Chat UI Tests (push) Waiting to run
Windows Unsloth Update CI / Unsloth Updating Tests (push) Waiting to run
Wheel CI / Wheel build + content sanity + import smoke (push) Waiting to run
GRPO: dispatch on width at the remaining lm_head matmul call sites (#8204)
* GRPO: dispatch on width at the remaining lm_head matmul call sites

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

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

* GRPO: read an explicit hidden-states signal when the widths cannot decide

The dispatch guard compared the forward's last dim against lm_head.shape[1],
which is the hidden size. On a model whose vocab_size equals its hidden size,
real logits satisfy that test and were sent through
chunked_hidden_states_selective_log_softmax, applying the lm_head a second
time and silently corrupting the log probabilities. The packed path's per-row
verifier made the same misclassification, so it compared corrupted against
corrupted and accepted the result.

Route all four call sites through _unsloth_grpo_returns_hidden_states, which
reads an explicit signal that the forward honoured UNSLOTH_RETURN_HIDDEN_STATES:
__UNSLOTH_SUPPORTS_RETURN_HIDDEN_STATES__ written by the zoo compiler, or the
_unsloth_grpo_hidden_states_forward_wrapped pair set by the rl.py fallback
wrapper. The width comparison stays: it is decisive whenever vocab_size differs
from hidden_size, and the signal is only consulted for the square case the
shape cannot answer, so an unsloth_zoo old enough to write no marker keeps
today's behaviour.

* GRPO: propagate hidden-state signal to gradient dispatches

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

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

* Track GRPO hidden-state degradation per call, and reject a half-patched zoo

The GRPO width dispatch reads two things the code assumed but did not hold.

_warn_grpo_hidden_states_fallback_once only ever set
_unsloth_grpo_hidden_states_warning_issued, so the flag the dispatch reads
after a forward meant "ever degraded", not "degraded on this call". Degradation
is per call: a forward that splats **kwargs into a sub-module only some inputs
reach rejects the hidden-state request on those batches and honours it on the
rest. With vocab_size == hidden_size the width test cannot correct that, so one
degraded batch sent every later hidden-state tensor to the raw-logits helper,
skipping the lm_head matmul. Record the outcome of each call in
_unsloth_grpo_hidden_states_degraded and keep the warning flag for warn-once
logging only; the signal reader falls back to the old flag when the attribute
is absent, so a stale generated trainer keeps working.

The fallback's TypeError retry also could not work: _drop_forward_kwargs_
consumed_positionally hands the caller's dict straight back when there is
nothing to drop, which every GRPO call site hits since they pass everything by
keyword, so adding output_hidden_states/return_dict poisoned the caller's
kwargs and the retry re-sent exactly what the model had just rejected. Copy
before mutating.

Finally, the source patch over zoo's gradient dispatches only failed when
nothing matched. A zoo that respells some of its dispatch sites still leaves
one the pattern recognises, which was enough to suppress the compatibility
error while the respelled sites kept deciding on width alone. Count the branch
headers that decide off an lm_head dimension and require that none survive the
substitution.

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

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

---------

Co-authored-by: Daniel Han <unslothshared@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: oobabooga <112222186+oobabooga@users.noreply.github.com>
2026-08-11 10:44:06 -07:00

88 lines
3.2 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
"""Load the GRPO hidden-states dispatch helpers straight out of the live source.
``_unsloth_grpo_returns_hidden_states`` and ``_unsloth_grpo_hidden_states_signal``
are shipped to the generated trainer as text (``RL_PRE_ITEMS``), so the tests
that ``exec`` a block of ``_get_per_token_logps_and_entropies`` need them in the
namespace exactly as the generated module would have them.
Lifting them with ``ast`` instead of importing ``unsloth`` keeps these tests
CPU-only and import-free, and keeps them tracking the shipped code rather than a
copy of it.
"""
from __future__ import annotations
import ast
import textwrap
from pathlib import Path
SOURCE_PATH = Path(__file__).resolve().parents[1] / "unsloth" / "models" / "rl_replacements.py"
HELPER_NAMES = (
"_unsloth_grpo_returns_hidden_states",
"_unsloth_grpo_hidden_states_signal",
)
def load_dispatch_helpers():
"""Return ``{name: function}`` for the helpers, exec'd from the live source."""
text = SOURCE_PATH.read_text(encoding = "utf-8")
tree = ast.parse(text, filename = str(SOURCE_PATH))
wanted = []
for name in HELPER_NAMES:
found = [
node
for node in tree.body
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == name
]
if len(found) != 1:
raise AssertionError(
f"expected exactly one module-level def {name} in {SOURCE_PATH}, found {len(found)}"
)
wanted.append(found[0])
namespace: dict = {}
exec(compile(ast.Module(body = wanted, type_ignores = []), str(SOURCE_PATH), "exec"), namespace)
return {name: namespace[name] for name in HELPER_NAMES}
def load_padded_loop_source():
"""Dedented source of the padded logprob loop, located structurally.
The one ``with`` statement inside ``_get_per_token_logps_and_entropies``
whose direct body holds ``for ... in zipped_inputs``. No text search, so a
comment quoting the same code cannot match.
"""
text = SOURCE_PATH.read_text(encoding = "utf-8")
tree = ast.parse(text, filename = str(SOURCE_PATH))
functions = [
node
for node in ast.walk(tree)
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
and node.name == "_get_per_token_logps_and_entropies"
]
if len(functions) != 1:
raise AssertionError(
f"expected exactly one def _get_per_token_logps_and_entropies, found {len(functions)}"
)
loops = [
node
for node in ast.walk(functions[0])
if isinstance(node, ast.With)
and any(
isinstance(stmt, ast.For)
and isinstance(stmt.iter, ast.Name)
and stmt.iter.id == "zipped_inputs"
for stmt in node.body
)
]
if len(loops) != 1:
raise AssertionError(f"expected exactly one padded loop, found {len(loops)}")
segment = ast.get_source_segment(text, loops[0], padded = True)
if segment is None:
raise AssertionError("could not recover the padded-loop source segment")
return textwrap.dedent(segment)