Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
599f051
Add ORPO (loss_type='orpo') for text models to MLXTrainer
BardiaKoopah Jun 24, 2026
2e3c22e
Add DPO (loss_type='dpo') with live LoRA-disable reference; share pre…
BardiaKoopah Jun 24, 2026
89108ff
Add MLXDPOTrainer/MLXORPOTrainer + configs (TRL-style API) over loss_…
BardiaKoopah Jun 25, 2026
3f14771
fix(mlx): ORPO SFT/NLL term over prompt+response to match TRL
BardiaKoopah Jul 1, 2026
72bb6c3
MLX ORPO/DPO: make preference-batch row order configurable
BardiaKoopah Jul 2, 2026
203919e
MLX ORPO/DPO: append EOS to preference completions (TRL parity)
BardiaKoopah Jul 2, 2026
7eda394
fail loud on full-FT DPO reference instead of silent collapse (DPO po…
BardiaKoopah Jul 2, 2026
818c23d
Merge remote-tracking branch 'origin/main' into pr830head
danielhanchen Jul 8, 2026
4035588
MLX ORPO: compute odds-ratio loss in float32 to avoid float16 NaN
shimmyshimmer Jul 8, 2026
d20e3ee
MLX ORPO/DPO: fix fast-tokenizer unwrap, LoRA reference collection, a…
danielhanchen Jul 8, 2026
ab26edd
MLX ORPO/DPO: preserve explicit warmup_steps in preference configs an…
danielhanchen Jul 8, 2026
aa4412d
MLX ORPO/DPO: preserve completion when truncating long preference rows
danielhanchen Jul 8, 2026
f45d4b2
DPO: reject DoRA adapters for referenced runs
danielhanchen Jul 9, 2026
62335b7
MLX ORPO/DPO: sample across lengths under max_steps; reject prebuilt …
danielhanchen Jul 9, 2026
397aa5a
MLX ORPO/DPO: torch-free default batching; clean DPO reference under …
danielhanchen Jul 9, 2026
1ec8819
MLX ORPO/DPO: cap preference batch padding at max_seq_length
danielhanchen Jul 9, 2026
eb6856d
MLX ORPO/DPO: fail-fast on unsupported loss_type, multi-GPU, and stre…
danielhanchen Jul 9, 2026
a5fa278
MLX ORPO/DPO: render conversational preference rows through the chat …
danielhanchen Jul 9, 2026
fc6117f
MLX ORPO/DPO: disable adapter dropout, thread chat kwargs, reshuffle …
danielhanchen Jul 9, 2026
26820e6
MLX ORPO/DPO: reshuffle torch_randperm per pass, disable all model dr…
danielhanchen Jul 9, 2026
2659c53
MLX ORPO/DPO: preserve saved lora_dropout, reshuffle epoch-based torc…
danielhanchen Jul 9, 2026
4b49b05
Merge branch 'main' into feat/mlx-orpo
BardiaKoopah Jul 28, 2026
fb2a822
Merge branch 'main' into feat/mlx-orpo
BardiaKoopah Jul 28, 2026
412c7bd
MLX preference: normalize content parts before chat templating
BardiaKoopah Jul 30, 2026
153164f
MLX preference: expose the one-pass batch count for epoch boundaries
BardiaKoopah Jul 30, 2026
003016c
MLX preference: tighten comments authored in this PR
BardiaKoopah Jul 30, 2026
a871293
MLX preference: compress reviewer comments in mlx/utils.py and traine…
BardiaKoopah Jul 30, 2026
de7a29e
MLX preference: compress reviewer regression notes in trainer tests
BardiaKoopah Jul 30, 2026
7ac5a2b
ci: execute the preference regression tests in the behavioral gate
BardiaKoopah Jul 30, 2026
f6f5709
MLX preference: tolerate the new fields in the config copy check
BardiaKoopah Jul 31, 2026
d3c00c8
MLX preference: forward callbacks= from the ORPO/DPO trainers
BardiaKoopah Jul 31, 2026
2b9d895
MLX preference: report real tokens, not the pair count, in telemetry
BardiaKoopah Jul 31, 2026
3d49c26
MLX preference: honor a fractional num_train_epochs
BardiaKoopah Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions tests/mlx_simulation/mlx_nn_stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,14 @@ def _init(shape, dtype=torch.float32):
init_module.uniform = _init_uniform


# ---------------------------------------------------------------------------
# Activations
# ---------------------------------------------------------------------------
def log_sigmoid(a, **kw):
"""mlx.nn.log_sigmoid(x) = log(sigmoid(x)); route to torch's stable impl."""
return F.logsigmoid(a)


# ---------------------------------------------------------------------------
# Module-level __getattr__: any unknown nn.X returns _Noop.
# ---------------------------------------------------------------------------
Expand Down
100 changes: 100 additions & 0 deletions tests/test_mlx_orpo_odds_ratio_stability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Unsloth Zoo - Utilities for Unsloth
# Copyright 2023-present Daniel Han-Chen, Michael Han-Chen & the Unsloth team. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

"""Regression test for ORPO odds-ratio numerical stability in float16.

The odds-ratio term uses log(1 - p) = log(-expm1(logp)) with a 1e-12 floor.
In float16 that floor underflows to 0.0, so a perfectly-predicted response row
(logp -> 0, e.g. an empty response span) makes mx.log(0.0) = -inf and yields
NaN gradients. The fix computes the term in float32 (mirroring TRL's
ORPOTrainer.odds_ratio_loss upcast). This test pins that a float16 logp row
with an exact 0.0 stays finite.
"""

from __future__ import annotations

import math
import sys

import pytest


@pytest.fixture(autouse=True, scope="module")
def _install_shim():
shim_prefixes = ("mlx", "mlx_lm", "mlx_vlm")
real_mlx_modules = {
name: module
for name, module in sys.modules.items()
if any(name == prefix or name.startswith(f"{prefix}.") for prefix in shim_prefixes)
}
from mlx_simulation import simulate_mlx_on_torch
from mlx_simulation.mlx_stub import _MLXFinder
simulate_mlx_on_torch()
for name in list(sys.modules):
if name == "unsloth_zoo.mlx" or name.startswith("unsloth_zoo.mlx."):
sys.modules.pop(name, None)
yield
for name in list(sys.modules):
if (
name == "unsloth_zoo.mlx" or name.startswith("unsloth_zoo.mlx.")
or any(name == prefix or name.startswith(f"{prefix}.") for prefix in shim_prefixes)
):
sys.modules.pop(name, None)
sys.meta_path[:] = [
finder for finder in sys.meta_path
if not isinstance(finder, _MLXFinder)
]
sys.modules.update(real_mlx_modules)


def test_naive_float16_odds_ratio_underflows_to_inf():
"""Document the bug: the pre-fix float16 expression is non-finite on logp=0."""
import mlx.core as mx

logp_c = mx.array([0.0], dtype=mx.float16) # perfectly-predicted chosen row
# Pre-fix formulation: floor built in the input (float16) dtype.
val_c = mx.maximum(-mx.expm1(logp_c), mx.array(1e-12, logp_c.dtype))
assert float(val_c.astype(mx.float32)[0]) == 0.0 # 1e-12 underflowed to 0.0
assert not math.isfinite(float(mx.log(val_c).astype(mx.float32)[0]))


def test_orpo_odds_ratio_loss_finite_on_perfect_float16_row():
"""The fixed helper stays finite on a float16 logp row containing 0.0."""
import mlx.core as mx
from unsloth_zoo.mlx.utils import _orpo_odds_ratio_loss

# Row 0: perfectly-predicted chosen response (logp -> 0). Row 1: ordinary.
logp_c = mx.array([0.0, -0.5], dtype=mx.float16)
logp_r = mx.array([-1.0, -2.0], dtype=mx.float16)
or_loss = _orpo_odds_ratio_loss(logp_c, logp_r)
val = float(or_loss.astype(mx.float32))
assert math.isfinite(val), f"ORPO odds-ratio loss not finite: {val}"
assert or_loss.dtype == mx.float32


def test_orpo_odds_ratio_loss_matches_float32_reference():
"""float16 inputs give the same result as float32 inputs (upcast parity)."""
import mlx.core as mx
from unsloth_zoo.mlx.utils import _orpo_odds_ratio_loss

logp_c16 = mx.array([-0.2, -0.5, -1.5], dtype=mx.float16)
logp_r16 = mx.array([-0.9, -2.0, -0.3], dtype=mx.float16)
logp_c32 = logp_c16.astype(mx.float32)
logp_r32 = logp_r16.astype(mx.float32)
a = float(_orpo_odds_ratio_loss(logp_c16, logp_r16).astype(mx.float32))
b = float(_orpo_odds_ratio_loss(logp_c32, logp_r32).astype(mx.float32))
assert math.isfinite(a) and math.isfinite(b)
assert abs(a - b) < 1e-2
Loading
Loading