Repository navigation
Add native MLX SimPO preference training with explicit objective parity #1999
Description
Source baseline
Soup origin/main at 3da204fadbd278c01ad5a4b23906b443d15e9dd1.
- Soup SimPO configuration and data contract
- Existing Transformers wrapper
- Current native task refusal
Problem and current evidence
task: simpo can train paired preferences on the Transformers path, but MLX rejects it at config load. The public loader refuses an otherwise valid dense config with task='simpo'; the matching Transformers control loads. SimPO offers preference alignment without a separate reference policy, useful on memory-constrained Apple Silicon.
Proposed initial scope and objective
- Implement native text-only causal SimPO with standard LoRA and
quantization: none, initially one documented architecture. Use existingtask: simpo,simpo_gammaandcpo_alpha; handle the existingtask: preference/preference_loss: simpodispatch consistently if supported, otherwise refuse that alias explicitly. No Torch/TRL model execution or backend fallback on the native path. - Match Soup's actual objective rather than silently substituting paper-only SimPO. Soup forwards
loss_type="simpo", gamma and the positive NLL weightcpo_alphato CPOConfig, but does not forward a beta field. At the supported TRL floor v0.29.0, beta defaults to 0.1 and label smoothing to zero: for completion-mean log probabilities a and b, the preference term ismean(softplus(-(beta * (a - b) - gamma))); total loss addscpo_alpha * chosen_token_NLL. The NLL is a mean over valid chosen tokens, not a mean of per-sequence means. Make these effective settings explicit and persist them. Do not repurposedpo_betaororpo_beta, remove the NLL, or enablecpo_alpha: 0without a separate schema/API decision. - Use causal shifting, prompt/padding exclusion and completion token counts consistently. Define EOS/chat-template behavior and independent per-answer truncation under the total
max_lengthcap. Refuse an answer with no supervised token after preprocessing before updates; do not copy known TRL truncation defects tracked in SimPO and KTO rows run past max_length on trl 0.29: the two wrappers never call enforce_preference_sequence_limit #1185/Preference prompt cap: should DPO and ORPO cut the prompt only when the row does not fit? #1209 (see active cap-fix PR Fix preference max length enforcement across TRL wrappers #1196) or regress SimPO trains to NaN, behind a logged loss of 0.0, whenchosenis longer thanmax_length#1208. Log preference and NLL components separately with meaningful aggregation. - Freeze base weights, train actual LoRA tensors, honor the supported optimizer/accumulation settings and save complete native adapter/tokenizer/objective metadata. Refuse unsupported quantization, full tuning, DoRA/LoRA+/FA and other task-specific combinations before costly loading until separately qualified. Do not claim DPO, online RL, VLM or every preference objective as part of this issue.
Acceptance tests
- Public config/train routing accepts the implemented slice; otherwise valid unsupported combinations fail with specific diagnostics before model/output mutation. Include the alias decision and preserve Transformers routing. Use paired prompt/chosen/rejected fixtures, malformed rows and unequal completion lengths.
- An independent numeric oracle verifies the preference term, chosen-token NLL, their weighted total and gradients on nonconstant logits with unequal lengths. Change gamma and cpo_alpha independently; summing instead of averaging completion scores, reversing the pair, omitting the margin or removing NLL must each break the appropriate assertion. Include padded batches, causal shifts and long prompts that formerly erased a completion.
- A tiny native public training run gives finite losses and actual LoRA updates while frozen tensors remain unchanged. Compare accumulated and equivalent effective batches under the stated reduction; check the final partial batch. The measured held-out preference score should move in the intended direction on a controlled learnable fixture, with a base/untrained control.
- Save and reload in a fresh native process, reproducing logits/scores and a nonzero adapter effect within documented tolerances. Missing/mismatched base or adapter metadata, failed writes and incompatible artifacts must give explicit errors without a complete-looking partial result. Separately qualify a pinned pretrained model and report pairwise held-out metrics without promising general alignment gains.
Coordination and alternatives
PR #1846 is native DPO, not SimPO. Reuse suitable routing, token scoring and artifact primitives if available without expanding that PR. #1977 owns exact training-state resume; #1985 owns broader message/history masking. Transformers is the current alternative. Versioned numerical references: TRL v0.29.0 CPO config and trainer. Audit later supported TRL versions before declaring cross-version parity.
Implementation validation and evidence limits
These are acceptance requirements for future implementation, not completed native qualification. Start with a relevant failing regression on the original source, exercise public entry points and real behavior, and verify test sensitivity by restoring the old behavior or removing the relevant condition. Assert results, gradients/updates, artifacts and specific refusal diagnostics; config acceptance, call counts and source-string checks alone do not establish support. Keep default tests offline and deterministic, without model downloads. Native tests must use real MLX operations; mocked or CPU-only tests do not qualify Apple Silicon training/inference.
Run the required non-smoke suite and clean Ruff for an implementation; update feature docs, supported-backend tables, changelog and generated references when affected. Explicitly select new native tests in the existing macOS MLX CI job (or an appropriately scoped evaluation/server job when extra dependencies are necessary); the current job does not discover every new native file automatically. Record source SHA, model/tokenizer revision, data/fixture identity, dependency versions, commands, numerical tolerances and passed/skipped/unexecuted checks. Qualify the declared dependency floor or add an explicit compatibility guard. Keep heavy imports lazy and preserve existing Transformers behavior.
This unclaimed proposal is for maintainer triage. It does not reserve work or start an implementation deadline. Maintainers retain area/difficulty assignment and the maintained-area first-refusal process. Preparation used pinned source/docs, live issue/PR duplicate checks and offline probes where stated; no native training, performance or pretrained-quality result is claimed.
Area
Training (SFT/DPO/GRPO)
Suggested difficulty
Not sure — maintainer triage requested.
Activity
MakazhanAlpamys commentedon Oct 19, 2026
Pointers to existing MLX code that may save time here, with one parity trap.
simpo_loss: https://github.com/ARahim3/mlx-tune/blob/c1133e37bf87/mlx_tune/losses.py#L511-L571. It length-normalises the log-probs and returns-log_sigmoid(beta * (r_chosen - r_rejected - gamma)), with defaults beta 2.0 and gamma 0.5. Its shared-prompt KV path (compute_log_probs_pair_shared_prefix, used only at batch size 1) is a speed idea worth a look.CPOTrainerwithloss_type="simpo"(src/soup_cli/trainer/simpo.py:207-209). TRL applies the margin outside the temperature:-logsigmoid(beta * (delta - simpo_gamma / beta)), i.e.beta * delta - simpo_gamma(trl 0.29.1,experimental/cpo/cpo_trainer.py:669-674). mlx-tune's form isbeta * delta - beta * gamma, so with Soup'ssimpo_gamma: 0.5and beta 2.0 a straight port would apply a margin of 1.0 instead of 0.5.cpo_alpha * NLL(chosen)(cpo_trainer.py:844), and Soup's defaultcpo_alphais 1.0. mlx-tune'ssimpo_losshas no NLL term.Any reused code keeps its Apache-2.0 attribution.
dryn85-max commentedon Oct 20, 2026
I'm taking this one (area:mlx). Hardware: Apple M4 Pro, 48 GB, mlx 0.32.3 / mlx-lm 0.32.0.
Plan
Order. This lands after feat(mlx): native DPO on Apple Silicon #1846 and reuses its row conversion, collation, completion masks,
sequence_logpsand adapter writer, so it is blocked on that PR and does not take a claim slot. It gets its owntrainer/mlx_simpo.py. It shares primitives with Add native MLX ORPO training with supervised and odds-ratio loss parity #2004 (ORPO, also claimed) but not objectives.Objective = Soup's effective one, pinned to TRL 0.29.1
experimental/cpo/cpo_trainer.py.a,b;mean(softplus(-(beta * (a - b) - simpo_gamma))). That is the margin outside the temperature, as you pointed out (beta*delta - gamma, not mlx-tune'sbeta*(delta - gamma));cpo_alpha * chosen NLL, with the NLL a mean over valid chosen tokens, not a mean of per-sequence means.Soup forwards no beta, so the effective
beta = 0.1(TRL's default) and label smoothing 0 are made explicit, written to the adapter metadata and shown in the docs.dpo_beta/orpo_betaare not reused, andcpo_alpha: 0stays as the schema has it. The exact NLL mask is quoted from TRL's labels in the PR.Scope. Text causal LoRA,
quantization: none, one documented architecture.task: simpoon mlx.task: preference/preference_loss: simpois refused explicitly on mlx. DoRA / LoRA+ / LoRA-FA, other quantization and full tuning are refused before loading. Preference and NLL are logged separately. An answer with no supervised token after preprocessing is refused before any update; truncation follows feat(mlx): native DPO on Apple Silicon #1846's caps, coordinated with SimPO and KTO rows run past max_length on trl 0.29: the two wrappers never call enforce_preference_sequence_limit #1185/Preference prompt cap: should DPO and ORPO cut the prompt only when the row does not fit? #1209/Fix preference max length enforcement across TRL wrappers #1196.Tests.
cpo_alphavaried independently.New native tests listed for
mlx-smoke;ci.ymluntouched.MakazhanAlpamys commentedon Oct 22, 2026
Fixed by #2097 (thanks @dryn85-max, and @Amix29 for the proposal and the parity notes).
task: simponow runs onbackend: mlx: Soup's own loop trains TRL 0.29.1's CPOsimpoobjective (-logsigmoid(0.1 * (a - b) - simpo_gamma)on mean completion log-probabilities, pluscpo_alphatimes the chosen-completion NLL), reference-free, with beta fixed at the value the transformers backend uses and written to the adapter metadata, and TRL-identical tokens on rows that fit indata.max_length. The preference and NLL terms are reported separately (preference_loss,nll_loss).quantization: 8bit,lora.r: 0, non-text modalities andtask: preferencewithpreference_loss: simpoare refused on MLX;--resumerefuses a checkpoint from another task or base.feat(mlx): native SimPO on Apple Silicon (MakazhanAlpamys#1999) (Maka…