Skip to content

Add native MLX SimPO preference training with explicit objective parity #1999

Closed

Description

@Amix29

Source baseline

Soup origin/main at 3da204fadbd278c01ad5a4b23906b443d15e9dd1.

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 existing task: simpo, simpo_gamma and cpo_alpha; handle the existing task: preference / preference_loss: simpo dispatch 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 weight cpo_alpha to 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 is mean(softplus(-(beta * (a - b) - gamma))); total loss adds cpo_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 repurpose dpo_beta or orpo_beta, remove the NLL, or enable cpo_alpha: 0 without 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_length cap. 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, when chosen is longer than max_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

  1. added
    needs-triageNot yet checked by a maintainer; removed when a difficulty label is set
    on Oct 17, 2026
  2. added
    enhancementNew feature or request
    difficulty:7Difficulty 7/10: new mechanism, needs a measurement.
    and removed
    needs-triageNot yet checked by a maintainer; removed when a difficulty label is set
    on Oct 18, 2026
  3. MakazhanAlpamys commented on Oct 19, 2026

    @MakazhanAlpamys
    Owner

    Pointers to existing MLX code that may save time here, with one parity trap.

    • mlx-tune (Apache-2.0) has 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.
    • Parity with Soup's transformers path is not direct. Soup runs SimPO through TRL's CPOTrainer with loss_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 is beta * delta - beta * gamma, so with Soup's simpo_gamma: 0.5 and beta 2.0 a straight port would apply a margin of 1.0 instead of 0.5.
    • TRL also adds cpo_alpha * NLL(chosen) (cpo_trainer.py:844), and Soup's default cpo_alpha is 1.0. mlx-tune's simpo_loss has no NLL term.
    • mlx-lm-lora (Apache-2.0) has no SimPO trainer: https://github.com/Goekdeniz-Guelmez/mlx-lm-lora/tree/main/mlx_lm_lora/trainer

    Any reused code keeps its Apache-2.0 attribution.

  4. dryn85-max commented on Oct 20, 2026

    @dryn85-max
    Contributor

    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_logps and adapter writer, so it is blocked on that PR and does not take a claim slot. It gets its own trainer/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.

      • completion-mean log-probs a, b;
      • preference term mean(softplus(-(beta * (a - b) - simpo_gamma))). That is the margin outside the temperature, as you pointed out (beta*delta - gamma, not mlx-tune's beta*(delta - gamma));
      • plus 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_beta are not reused, and cpo_alpha: 0 stays 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: simpo on mlx. task: preference / preference_loss: simpo is 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.

      • A NumPy oracle for the preference term, NLL, total and gradients on unequal-length logits, with gamma and cpo_alpha varied independently.
      • Mutations, each failing a named test: summed instead of averaged scores, reversed pair, no margin, no NLL, margin inside the temperature.
      • Padded batches and long prompts.
      • Tiny native training with real LoRA updates and frozen tensors unchanged, accumulation equivalence, the final partial batch, a held-out preference score moving the right way against an untrained control.
      • Fresh-process reload, with mismatched metadata refused.
    • New native tests listed for mlx-smoke; ci.yml untouched.

  5. MakazhanAlpamys commented on Oct 22, 2026

    @MakazhanAlpamys
    Owner

    Fixed by #2097 (thanks @dryn85-max, and @Amix29 for the proposal and the parity notes). task: simpo now runs on backend: mlx: Soup's own loop trains TRL 0.29.1's CPO simpo objective (-logsigmoid(0.1 * (a - b) - simpo_gamma) on mean completion log-probabilities, plus cpo_alpha times 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 in data.max_length. The preference and NLL terms are reported separately (preference_loss, nll_loss). quantization: 8bit, lora.r: 0, non-text modalities and task: preference with preference_loss: simpo are refused on MLX; --resume refuses a checkpoint from another task or base.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:mlxThe MLX backenddifficulty:7Difficulty 7/10: new mechanism, needs a measurement.enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions