Phase 18 / Post-training: SFT, LoRA, reward models and RLHF
Direct preference optimization and reference-corrected margins
Revision v0.7.0 · Updated since your last visit. Review the changes; your progress is saved.
Lesson · about 105 minutes · CPU practice
Compare chosen and rejected response log probabilities. Subtract the reference preference.
Phase study guide: preparation, milestones and project evidence
Before you start
Before this: RLHF mechanics: a frozen reward and PPO policy updates
Hardware: CPU
Setup help: Python, notebook, or Cloud TPU VM
Use the tested CPU packages (the recorded environment uses Python 3.14.3). Other Python versions and accelerator environments require separate checks. The optional Cloud TPU specialization explains the target-specific execution boundaries. Logical CPU devices practice partitioning; they do not simulate TPU performance.
Download the repository ZIP or use your existing checkout. Extract it and open a terminal in the folder containing requirements-cpu.txt. The beginner ZIP is separate; these lessons use the full repository folder.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-cpu.txtpy -3 -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements-cpu.txtAfter activation, run the complete worked example from that same folder:
python phases/18-posttraining/05-dpo/code/main.py.\.venv\Scripts\python.exe phases/18-posttraining/05-dpo/code/main.pyFor an incremental build, create main.py in your own practice folder, append each build block in order, and run python main.py with this environment active. Keep that file separate from the downloaded reference solution.
Download this lesson’s notebook. Open it with a Python kernel using the course environment. Run cells from the top after restarting the kernel; experiments depend on earlier definitions.
For local Jupyter, first install the course requirements using the Local Python steps, then use your environment’s Python to start Jupyter:
python -m pip install jupyterlab
python -m jupyter lab.\.venv\Scripts\python.exe -m pip install jupyterlab
.\.venv\Scripts\python.exe -m jupyter labSelect the downloaded posttraining-05.ipynb file in JupyterLab. The notebook contains reference solutions; make your prediction and attempt each practice task before opening them. Restart the kernel before changing logical device setup.
The worked reference for this lesson is CPU-validated. Some references explicitly configure logical CPU devices; setting JAX_PLATFORMS does not adapt them to a TPU. Follow the execution matrix and reviewed multi-host example before attempting a target run.
For accelerator preparation, study TPU setup, observed runtime receipts, checkpointed jobs and precision and retained traces. These target paths remain unqualified on physical TPU hardware until an actual receipt is recorded. Cloud provisioning is optional for CPU learning.
Check: Reference-corrected margins become positive and the initial equal-policy loss is log(2).
Revisit the guided CPU setup · Open 19: TPU Setup. Package instructions are CPU instructions; Windows and accelerator environments need their own validation.
What you will be able to do
- Compare chosen and rejected response log probabilities
- Subtract the reference preference
- Verify the changed-condition exercise and explain the limits of this fixture.
The problem
Can we train on chosen/rejected responses without collecting fresh reward-model rollouts? DPO provides a different training objective. We will work through the reference correction and compare its bookkeeping with the preceding RLHF loop.
The idea
DPO uses chosen/rejected preferences and a fixed reference to optimize a policy directly. It compares policy log-probability differences with reference differences. This training step does not require an explicit reward-model call or on-policy rollout.
A better relative margin need not raise chosen probability
Suppose chosen probability falls from to while rejected probability falls from to . The chosen-to-rejected ratio rises from to , even though the chosen probability decreased. Relative preference improvement and absolute likelihood improvement are different claims.
The reference corrects that log-ratio margin, and beta scales it in the loss. Keep the reference checkpoint and any cached reference probabilities bound to the same examples and preprocessing.
Read the margin plot alongside individual probabilities when diagnosing behavior. A growing training margin is separate from held-out preference quality or generation quality. The categorical fixture teaches the objective; sequence integration also needs token masking and log-probability aggregation checks.
DPO corrects a policy margin with a reference
Predict: What extra evidence would you inspect before claiming a DPO update makes chosen responses more likely?
Both models score the same chosen/rejected pair. Their log-probability differences form a reference-corrected margin, which beta scales before the stable preference loss. The reference is frozen. There is no reward-model call or on-policy rollout in this step.
Before coding: compare changes in preference, not raw scores
Consider a prompt with one chosen and one rejected response. The reference already prefers one of them to some degree. DPO asks how the current policy changes that relative preference. Write four log probabilities: current chosen, current rejected, reference chosen and reference rejected. Take a within-model difference first, then subtract the reference difference.
When current and reference models are identical, both differences cancel even if neither response has high probability. The loss starts at . This cancellation is a precise implementation check, not evidence that the initial model is indifferent between the two responses.
Compare chosen and rejected response log probabilities
Both responses belong to the same prompt. Compute their sequence log probabilities under current and reference policies. Exclude prompt and padding tokens consistently. Keep lengths, tokenization and special-token rules visible because they determine the probability of the represented response.
Subtract the reference preference
The reference-corrected margin subtracts what the reference already preferred. At initialization when current policy equals reference, the corrected margin is zero and the loss is , even if the reference assigns unequal response probabilities.
Work through a numeric margin and its gradient
Suppose current chosen/rejected log probabilities are and , while reference values are and . The current gap is , the reference gap is , and the corrected margin is . With , the loss is , about . Reversing chosen and rejected flips the corrected margin.
Let denote the corrected margin. Its derivative below is negative for finite , so gradient descent increases the chosen-versus-rejected gap on this pair. The underlying model shares parameters across many responses, so this local pressure does not guarantee every chosen response’s absolute probability rises.
A rising preference ratio can hide falling chosen probability
Imagine reference probabilities for chosen, rejected and another response. A new policy lowers the chosen probability but lowers the rejected probability even more. The chosen/rejected ratio grows from one to three, so the DPO loss improves for this comparison.
Track chosen and rejected sequence log probabilities separately, not only the margin. Also evaluate useful task behavior, response length and diversity on held-out prompts. Pairwise training is a relative objective; it does not directly specify the probability allocated to every unpaired response.
Keep the algorithm distinction visible
This loop updates from fixed preference pairs; it has no separately trained reward model or on-policy rollout stage. That does not make it PPO with a different optimizer. Both methods need a base/reference identity and reliable preference data, but their estimators, data freshness and failure modes differ.
Treat beta and length as part of the objective
Beta scales the corrected margin and changes update behavior; it is not interchangeable with every explicit KL coefficient in a PPO implementation. Response sums and response means also differ. Make comparisons under a declared objective and evaluation protocol instead of selecting hyperparameters from a training-loss curve alone.
Evaluate beyond chosen-pair fit
Frozen-pair optimization can overfit or reduce useful response diversity. Retain held-out prompts, independent task metrics and reviewed outputs; inspect slices rather than only average preference accuracy. Synthetic action preferences establish an objective implementation test, not an aligned language model.
Carry sequence bookkeeping and reference identity into a real model
For language responses, add log probabilities over valid answer and end tokens while conditioning on the prompt. Exclude padding and prompt targets consistently. A mean over response length changes the objective: two equally likely tokens have twice the negative log probability of one. Decide which objective you intend rather than introducing normalization as an innocent convenience.
Reference caches must bind prompt text, chosen/rejected ordering, tokenizer, chat template, mask and checkpoint revision. A frozen array can still be wrong if its examples were reordered. Split prompt groups before fitting, compare fixed decoding on held-out tasks, and retain the actual reference artifact so a later run can reproduce the correction. This categorical lab supplies objective checks, not the sequence trainer.
Direct preference optimization and reference-corrected margins — recorded experiment
Predict before looking: What does a positive corrected margin establish, and what does it leave unknown about absolute probabilities?
Read the figure
The line records preference loss before each update, starting at and falling to about . It measures fit to the same three synthetic action comparisons. Its numerical scale depends on beta and does not directly compare with reward-model NLL or PPO reward.
The second panel shows final reference-corrected margins for each comparison. All positive bars mean those pairwise ratios improved relative to the reference. The zero-versus-two margin combines the other two gaps. A positive bar does not by itself prove the absolute probability of the chosen response increased; the separate three-action counterexample demonstrates why. Read these as log-ratio differences, not probabilities or human preference scores.
Connect it to the computation
Positive final corrected margins mean the policy favors each chosen action more strongly relative to the reference. The independent host softplus calculation and reference-gradient check establish objective behavior; separate generation and human review would be needed for a language application.
Open full-size figureDownload data and provenance
The notebook contains the data experiment, plotting code, and recorded figure output. Rerun it to test your changes.
Where to put & run this code: Work in phases/18-posttraining/05-dpo/code/main.py (or downloaded exercises/posttraining-05.py / exercises/posttraining-05.ipynb) and execute from the repository root with python3 phases/18-posttraining/05-dpo/code/main.py.
1 Define the reference-corrected pair objective
Create main.py in your activated course environment. Paste this block, then run python main.py; function definitions alone print nothing.
# Step 1 — 1. Define the reference-corrected pair objective: The reference correction is detached, while current policy log...
import jax
import jax.numpy as jnp
import numpy as np
# Define `dpo_loss(policy_logps, reference_logps, chosen, rejected...)` to evaluate the objective and its automatic derivatives:
def dpo_loss(policy_logps, reference_logps, chosen, rejected, beta=0.2):
margin = (policy_logps[chosen] - policy_logps[rejected]) - jax.lax.stop_gradient(
reference_logps[chosen] - reference_logps[rejected]
)
return jnp.mean(jax.nn.softplus(-beta * margin))Code building blocks in this step
x.sum(axis=...) / x.mean(axis=..., keepdims=...)— Reduces values along the namedaxis(the axis you name is collapsed unlesskeepdims=True).
The reference correction is detached, while current policy log probabilities remain differentiable.
2 Freeze reference probabilities and comparison IDs
Append this block to main.py and run python main.py again. Keep the earlier blocks above it.
# Step 2 — 2. Freeze reference probabilities and comparison IDs: The initial policy equals a nonuniform reference.
reference = jax.nn.log_softmax(jnp.array([0.2, 0.0, -0.2]))
chosen = jnp.array([0, 0, 1])
rejected = jnp.array([1, 2, 2])
theta = jnp.array([0.2, 0.0, -0.2])
history = []
# Evaluate numerically stable log-space cross-entropy/likelihood (`loss`).
loss = lambda theta: dpo_loss(
jax.nn.log_softmax(theta), reference, chosen, rejected, 0.3
)
# Differentiate the objective to obtain `step` via automatic differentiation.
step = jax.jit(jax.value_and_grad(loss))
np.testing.assert_allclose(loss(theta), np.log(2), atol=1e-6)Code building blocks in this step
jnp.array(values, dtype=...)— Constructs an immutable device-backed JAX array from Python/NumPy values.jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.jax.value_and_grad(loss_fn)(params, ...)— Evaluates both the scalar loss and its gradient PyTree(loss_val, grads)in a single forward+backward pass.jax.jit(fn) / @jax.jit— Tracesfnwith abstract shapes and compiles a fused XLA executable cached by input shape and dtype.
The initial policy equals a nonuniform reference. Predict the corrected margin and initial loss before continuing.
3 Fit the policy and test cancellation
Append this block to main.py and run python main.py again. Keep the earlier blocks above it.
# Step 3 — 3. Fit the policy and test cancellation: The host stable-softplus calculation checks final margins.
for _ in range(120):
value, g = step(theta)
history.append(float(value))
theta = theta - 0.4 * g
assert history[-1] < history[0] * 0.5
# Evaluate numerically stable log-space cross-entropy/likelihood (`policy`).
policy = jax.nn.log_softmax(theta)
# Convert `margin` to a host NumPy array for inspection or verification.
margin = np.asarray(
(policy[chosen] - policy[rejected]) - (reference[chosen] - reference[rejected]),
np.float64,
)
# Execute `np.testing.assert_allclose(`.
np.testing.assert_allclose(
loss(theta), np.mean(np.logaddexp(0, -0.3 * margin)), atol=1e-6
)
assert np.all(margin > 0)
np.testing.assert_allclose(loss(theta + 50), loss(theta), atol=1e-6)
print(
'DPO initial/final:',
history[0],
history[-1],
'; reference-corrected margins:',
margin,
)Code building blocks in this step
x.sum(axis=...) / x.mean(axis=..., keepdims=...)— Reduces values along the namedaxis(the axis you name is collapsed unlesskeepdims=True).jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.
The host stable-softplus calculation checks final margins. Adding a common logit offset preserves the probability distribution and therefore the objective.
View the cumulative build in main.py
These blocks append in order to the same file.
# Step 1 — 1. Define the reference-corrected pair objective: The reference correction is detached, while current policy log...
import jax
import jax.numpy as jnp
import numpy as np
# Define `dpo_loss(policy_logps, reference_logps, chosen, rejected...)` to evaluate the objective and its automatic derivatives:
def dpo_loss(policy_logps, reference_logps, chosen, rejected, beta=0.2):
margin = (policy_logps[chosen] - policy_logps[rejected]) - jax.lax.stop_gradient(
reference_logps[chosen] - reference_logps[rejected]
)
return jnp.mean(jax.nn.softplus(-beta * margin))
# Step 2 — 2. Freeze reference probabilities and comparison IDs: The initial policy equals a nonuniform reference.
reference = jax.nn.log_softmax(jnp.array([0.2, 0.0, -0.2]))
chosen = jnp.array([0, 0, 1])
rejected = jnp.array([1, 2, 2])
theta = jnp.array([0.2, 0.0, -0.2])
history = []
# Evaluate numerically stable log-space cross-entropy/likelihood (`loss`).
loss = lambda theta: dpo_loss(
jax.nn.log_softmax(theta), reference, chosen, rejected, 0.3
)
# Differentiate the objective to obtain `step` via automatic differentiation.
step = jax.jit(jax.value_and_grad(loss))
np.testing.assert_allclose(loss(theta), np.log(2), atol=1e-6)
# Step 3 — 3. Fit the policy and test cancellation: The host stable-softplus calculation checks final margins.
for _ in range(120):
value, g = step(theta)
history.append(float(value))
theta = theta - 0.4 * g
assert history[-1] < history[0] * 0.5
# Evaluate numerically stable log-space cross-entropy/likelihood (`policy`).
policy = jax.nn.log_softmax(theta)
# Convert `margin` to a host NumPy array for inspection or verification.
margin = np.asarray(
(policy[chosen] - policy[rejected]) - (reference[chosen] - reference[rejected]),
np.float64,
)
# Execute `np.testing.assert_allclose(`.
np.testing.assert_allclose(
loss(theta), np.mean(np.logaddexp(0, -0.3 * margin)), atol=1e-6
)
assert np.all(margin > 0)
np.testing.assert_allclose(loss(theta + 50), loss(theta), atol=1e-6)
print(
'DPO initial/final:',
history[0],
history[-1],
'; reference-corrected margins:',
margin,
)Run the example
python3 phases/18-posttraining/05-dpo/code/main.pyAbout this command
This shortcut uses an activated macOS/Linux course environment in the repository folder. For Windows, use the explicit interpreter command in “Setup help” above. The script includes worked examples and reference solutions.
Inspect the complete assembled example
# Step 1 — 1. Define the reference-corrected pair objective: The reference correction is detached, while current policy log...
import jax
import jax.numpy as jnp
import numpy as np
# Define `dpo_loss(policy_logps, reference_logps, chosen, rejected...)` to evaluate the objective and its automatic derivatives:
def dpo_loss(policy_logps, reference_logps, chosen, rejected, beta=0.2):
margin = (policy_logps[chosen] - policy_logps[rejected]) - jax.lax.stop_gradient(
reference_logps[chosen] - reference_logps[rejected]
)
return jnp.mean(jax.nn.softplus(-beta * margin))
# Step 2 — 2. Freeze reference probabilities and comparison IDs: The initial policy equals a nonuniform reference.
reference = jax.nn.log_softmax(jnp.array([0.2, 0.0, -0.2]))
chosen = jnp.array([0, 0, 1])
rejected = jnp.array([1, 2, 2])
theta = jnp.array([0.2, 0.0, -0.2])
history = []
# Evaluate numerically stable log-space cross-entropy/likelihood (`loss`).
loss = lambda theta: dpo_loss(
jax.nn.log_softmax(theta), reference, chosen, rejected, 0.3
)
# Differentiate the objective to obtain `step` via automatic differentiation.
step = jax.jit(jax.value_and_grad(loss))
np.testing.assert_allclose(loss(theta), np.log(2), atol=1e-6)
# Step 3 — 3. Fit the policy and test cancellation: The host stable-softplus calculation checks final margins.
for _ in range(120):
value, g = step(theta)
history.append(float(value))
theta = theta - 0.4 * g
assert history[-1] < history[0] * 0.5
# Evaluate numerically stable log-space cross-entropy/likelihood (`policy`).
policy = jax.nn.log_softmax(theta)
# Convert `margin` to a host NumPy array for inspection or verification.
margin = np.asarray(
(policy[chosen] - policy[rejected]) - (reference[chosen] - reference[rejected]),
np.float64,
)
# Execute `np.testing.assert_allclose(`.
np.testing.assert_allclose(
loss(theta), np.mean(np.logaddexp(0, -0.3 * margin)), atol=1e-6
)
assert np.all(margin > 0)
np.testing.assert_allclose(loss(theta + 50), loss(theta), atol=1e-6)
print(
'DPO initial/final:',
history[0],
history[-1],
'; reference-corrected margins:',
margin,
)Expected result: Reference-corrected margins become positive and the initial equal-policy loss is log(2).
Recorded reference execution
Reference output captured from executing the complete lesson script (code/main.py) and its assertions in the course CPU environment.
DPO initial/final: 0.6931471824645996 0.27227783203125 ; reference-corrected margins: [3.0890286 6.17805719 3.08902884]
DPO initial/final: 0.6931471824645996 0.27227783203125 ; reference-corrected margins: [3.0890286 6.17805719 3.08902884]
Corrected/unadjusted initial loss: 0.6931471824645996 0.6540467739105225
Loss before/after: 0.6931471824645996 0.5418724417686462 ; chosen probability: 0.4 -> 0.3
Reversed-label loss: 1.5064733028411865
Reference gradient is zero.
One-pair logit gradient agrees with independent margin derivative.
PASS: posttraining-05
Download execution record · The notebook includes executed cells and figure outputs. Compare your run with these values; timings depend on the machine.
Verify the reference cancellation
# Experiment — Verify the reference cancellation: The initial policy is not uniform.
initial = dpo_loss(reference, reference, chosen, rejected, 0.3)
np.testing.assert_allclose(initial, np.log(2), atol=1e-6)
# Aggregate array values to compute `uncorrected`.
uncorrected = jnp.mean(
jax.nn.softplus(-0.3 * (reference[chosen] - reference[rejected]))
)
assert not np.isclose(float(initial), float(uncorrected))
print('Corrected/unadjusted initial loss:', float(initial), float(uncorrected))Expected: The corrected initial loss is log(2); omitting the reference correction changes it.
The initial policy is not uniform. Cancellation, rather than equal response probabilities, makes the corrected margin zero.
Improve the ratio while lowering the chosen probability
# Experiment — Improve the ratio while lowering the chosen probability: The rejected probability falls further.
example_ref = jnp.log(jnp.array([0.4, 0.4, 0.2]))
example_policy = jnp.log(jnp.array([0.3, 0.1, 0.6]))
choice = jnp.array([0])
reject = jnp.array([1])
before = float(dpo_loss(example_ref, example_ref, choice, reject, 0.3))
after = float(dpo_loss(example_policy, example_ref, choice, reject, 0.3))
assert after < before and float(jnp.exp(example_policy[0])) < float(
jnp.exp(example_ref[0])
)
np.testing.assert_allclose(after, np.logaddexp(0.0, -0.3 * np.log(3)), atol=1e-6)
print('Loss before/after:', before, after, '; chosen probability: 0.4 -> 0.3')Expected: The DPO loss decreases while the chosen probability falls from 0.4 to 0.3.
The rejected probability falls further. Monitoring only the margin would miss the shift toward an unpaired response.
Make it yours
Where to put your code: Append your solution at the bottom of phases/18-posttraining/05-dpo/code/main.py (or inside the Make it yours cell of exercises/posttraining-05.ipynb) and run python3 phases/18-posttraining/05-dpo/code/main.py to verify your prediction before opening the reference solution.
Swap chosen and rejected IDs after training and verify that the loss rises.
Show a writing guide and starter scaffold
How to write this exercise code: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
dpo_loss(...)— Calldpo_losswith your updated parameters or inputs from this lesson's workspace.assert condition— Verify that the observed output shape, status, or numerical value satisfies the contract.
2. Step-by-step implementation plan:
- Set up the changed input or configuration parameters requested in the prompt.
- Run the target function or transformation and assert that the output matches your prediction.
# Exercise solution: Swap chosen and rejected IDs after training and verify that the loss...
reverse = float(...) # TODO: compute reverse
assert reverse # TODO: complete assertion check
print('Reversed-label loss:', reverse)
Reveal the reference solution
# Exercise solution: Swap chosen and rejected IDs after training and verify that the loss...
reverse = float(dpo_loss(policy, reference, rejected, chosen, 0.3))
assert reverse > float(dpo_loss(policy, reference, chosen, rejected, 0.3))
print('Reversed-label loss:', reverse)Freeze the reference numerically and in autodiff
Transfer
Differentiate with respect to reference log probabilities and verify that their gradient is zero.
Show a writing guide and starter scaffold
How to write this practice solution: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
jax.grad(loss_fn)(params, ...)— Transforms a scalar-output function into a function returning the gradient PyTree with the same structure asparams.
2. Step-by-step implementation plan:
- Differentiate the objective to obtain
reference_gradvia automatic differentiation. - Allocate initialized array
with the specified shape and dtype.
# Freeze the reference numerically and in autodiff (Transfer): Also retain the actual reference checkpoint and tokenizer...
# Differentiate the objective to obtain `reference_grad` via automatic differentiation.
reference_grad = jax.grad(...) # TODO: compute reference_grad
policy, reference, chosen, rejected, 0.3
)
# Allocate initialized array `` with the specified shape and dtype.
np.testing.assert_array_equal(
reference_grad, np.zeros(reference_grad.shape)
)
print('Reference gradient is zero.')
Show a hint
The reference is an input to the objective but not an optimization variable.
Reveal reference solution and reasoning
# Freeze the reference numerically and in autodiff (Transfer): Also retain the actual reference checkpoint and tokenizer...
# Differentiate the objective to obtain `reference_grad` via automatic differentiation.
reference_grad = jax.grad(dpo_loss, argnums=1)(
policy, reference, chosen, rejected, 0.3
)
# Allocate initialized array `` with the specified shape and dtype.
np.testing.assert_array_equal(
reference_grad, np.zeros(reference_grad.shape)
)
print('Reference gradient is zero.')Also retain the actual reference checkpoint and tokenizer identity. A detached but reloaded different model is still a changed reference.
Derive a one-pair categorical gradient
Challenge
At a fresh three-action policy, compute the corrected margin and derive the logit gradient for one chosen/rejected pair. Why does the third action have zero direct logit gradient here?
Show a writing guide and starter scaffold
How to write this practice solution: step-by-step recipe & starter scaffold
1. Key functions & syntax to use:
jnp.array(values, dtype=...)— Constructs an immutable device-backed JAX array from Python/NumPy values.jnp.allclose(actual, expected, rtol=..., atol=...)— Checks that two arrays match elementwise within floating-point tolerance.jax.grad(loss_fn)(params, ...)— Transforms a scalar-output function into a function returning the gradient PyTree with the same structure asparams.
2. Step-by-step implementation plan:
- Evaluate numerically stable log-space cross-entropy/likelihood (
one_pair). - Differentiate the objective to obtain gradients
.
# Derive a one-pair categorical gradient (Challenge): The unpaired logit is absent from this categorical...
probe = jnp.array(...) # TODO: compute probe
choice = jnp.array(...) # TODO: compute choice
reject = jnp.array(...) # TODO: compute reject
beta = ... # TODO: compute beta
# Evaluate numerically stable log-space cross-entropy/likelihood (`one_pair`).
one_pair = ... # TODO: compute one_pair
jax.nn.log_softmax(logits), reference, choice, reject, beta
)
margin_value = float(...) # TODO: compute margin_value
factor = ... # TODO: compute factor
# Differentiate the objective to obtain gradients ``.
np.testing.assert_allclose(
jax.grad(one_pair)(probe), [factor, -factor, 0.0], atol=1e-6
)
print('One-pair logit gradient agrees with independent margin derivative.')
Show a hint
The log-softmax normalizer cancels in a log-probability difference. Shared model parameters make real sequence updates less isolated.
Reveal reference solution and reasoning
# Derive a one-pair categorical gradient (Challenge): The unpaired logit is absent from this categorical...
probe = jnp.array([0.6, -0.3, 0.1])
choice = jnp.array([0])
reject = jnp.array([1])
beta = 0.3
# Evaluate numerically stable log-space cross-entropy/likelihood (`one_pair`).
one_pair = lambda logits: dpo_loss(
jax.nn.log_softmax(logits), reference, choice, reject, beta
)
margin_value = float((probe[0] - probe[1]) - (reference[0] - reference[1]))
factor = -beta / (1 + np.exp(beta * margin_value))
# Differentiate the objective to obtain gradients ``.
np.testing.assert_allclose(
jax.grad(one_pair)(probe), [factor, -factor, 0.0], atol=1e-6
)
print('One-pair logit gradient agrees with independent margin derivative.')The unpaired logit is absent from this categorical difference, yet its normalized probability can change when the other logits move.
Diagnose the result
If initial loss is not log(2) when current equals reference, inspect the subtraction and masks. If reversed preferences still improve the reported score, inspect label direction and cached-reference alignment.
Carry forward
- Subtract the frozen reference log-probability gap from the policy gap so the corrected margin is zero and the initial DPO loss equals whenever .
- Monitor chosen and rejected sequence log probabilities alongside the reference-corrected margin, because improving the relative ratio does not guarantee that absolute chosen probability increases.
Keep your evidence
Keep reference identity, response log-probability conventions, log(2) initialization, independent margin loss, reversed labels and frozen-reference checks.
Save your modified code, its output, and an explanation of your prediction. Checkpoint answers and exercise evidence are tracked separately.
Saved in this browser. Export or back up your portfolio anytime in My learning.
Verified on local CPU. Stage to a Cloud TPU VM via Cloud TPU setup to record accelerator execution receipts (TODO: add automated Cloud TPU CI execution receipts).
Primary references
Phase 18 Integration Project
Bring this phase together: Audit response-token SFT masking and preference-pair loss
Checkpoint to prove: Complete stages 4–8 of training-methods. Retain shifted token-mask checks, frozen-base/merge evidence, preference oracles, signed PPO clipping and fixed-reference DPO comparisons.
Work in projects/posttraining-audit/starter/model.py. Complete stages 1 and 2 for independent calculations and changed-condition diagnosis, then retain the stage 3 evidence with --evidence-dir runs/posttraining-audit. Stage 4 integrates the checks. Learner commands explicitly select the starter; --implementation solution is instructor reference execution. This bounded practice does not certify the entire phase or target hardware.