(GRPO) Fix PEFT replacement for TRL >= 1.7.0, add missing compute_aux_loss for TRL >= 1.7.0#6904
Conversation
marcandrelarochelle
commented
Jul 6, 2026
- Fix PEFT replacement for TRL >= 1.7.0
- Add missing compute_aux_loss for TRL >= 1.7.0
There was a problem hiding this comment.
Code Review
This pull request introduces support for TRL version 1.7.0+ by adjusting PEFT initialization logic in unsloth/models/rl.py, and adds auxiliary loss computation support (compute_aux_loss) to _get_per_token_logps_and_entropies in unsloth/models/rl_replacements.py. However, a critical bug was identified in unsloth/models/rl_replacements.py where _extra_moe_kwargs is not initialized before a key is assigned to it, which will lead to a NameError at runtime.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if compute_aux_loss is not None: | ||
| _extra_moe_kwargs["output_router_logits"] = compute_aux_loss | ||
| else: | ||
| _extra_moe_kwargs = {} |
There was a problem hiding this comment.
A NameError will occur here because _extra_moe_kwargs is not initialized before we attempt to assign a key to it in the if branch. Please initialize _extra_moe_kwargs as an empty dictionary first, or use a dictionary comprehension/conditional assignment.
_extra_moe_kwargs = {"output_router_logits": compute_aux_loss} if compute_aux_loss is not None else {}There was a problem hiding this comment.
Confirmed and fixed. Initializing _extra_moe_kwargs before use, and gating on truthiness of compute_aux_loss (which is False, never None, for non-MoE under TRL 1.7.x) so router logits are only requested for MoE.
|
Thanks for this. I pushed a few fixes on top and validated the branch end to end with a tiny Qwen3-MoE GRPO run on TRL 1.7.1 (also transformers 5.8 / torch 2.10). Summary of what changed and why. Fixes pushed
With these, a Qwen3-MoE GRPO run trains to completion under TRL 1.7.1 where the previous state raised the unpack error. Follow-up needed (MoE aux loss is not yet functional)Heads up that the aux-loss piece does not yet actually apply the router load-balancing loss in Unsloth's GRPO. On the optimized path Unsloth returns hidden states and the model forward does not populate Making it functional is best done at the model-forward chokepoint (populate |
|
@danielhanchen Sorry for the small bug with the I tested the new changes you made and I can confirm it works with TRL 1.7.0, haven't updated to 1.7.1 yet, but will pretty soon! If you have time, I also created 2 PR on Unsloth Zoo (for the SAPO + Luspo loss and one for the Importance Sampling + VESPO bug fix -> needed for TRL >= 1.6.0) |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3e9a3b20ed
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| aux_loss = ( | ||
| torch.stack(all_aux_losses).mean() | ||
| if (compute_aux_loss and len(all_aux_losses) > 0) | ||
| else None |
There was a problem hiding this comment.
Return aux_loss when packed GRPO path is used
For TRL >= 1.7 MoE training with compute_aux_loss=True, the default sequence-packing or PrefixGrouper path sets logprobs and empties zipped_inputs, so the padded loop that appends to all_aux_losses never runs. This expression then returns None for aux_loss, and TRL's loss path expects a tensor to add/log the router auxiliary loss, so packed text batches either crash or silently lose the MoE aux objective unless packing falls back.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 4079206: return a zero placeholder (logprobs.new_zeros(())) instead of None whenever compute_aux_loss is set, so the packed/PrefixGrouper path no longer yields None and TRL can add/log the term without a crash. The router aux objective itself is not yet wired into the Unsloth GRPO loss; that is tracked as a separate unsloth_zoo change.
| if compute_aux_loss and getattr(outputs, "aux_loss", None) is not None: | ||
| all_aux_losses.append(outputs.aux_loss) |
There was a problem hiding this comment.
Avoid collecting aux_loss under inference mode
When sequence packing falls back and compute_aux_loss=True, this aux loss is produced inside the surrounding _get_inference_mode_context_manager(model) block, so it is detached from autograd before being returned. In the padded fallback path TRL can add/log a tensor, but the router auxiliary objective contributes no gradients, which means MoE load-balancing is silently not trained whenever this path is used.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 4079206: removed the aux_loss collection that ran inside the inference-mode block, so no detached tensor is returned. aux_loss is now a zero placeholder pending the separate change that computes the router loss with gradients.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 407920673f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # The MoE router aux loss is not yet wired into the Unsloth GRPO loss, so return a | ||
| # zero placeholder (never None) to keep the TRL >= 1.7.0 3-tuple contract and let TRL | ||
| # add/log it without a crash. | ||
| aux_loss = logprobs.new_zeros(()) if compute_aux_loss else None |
There was a problem hiding this comment.
Return the real MoE router auxiliary loss
When GRPO runs with TRL >= 1.7 on an MoE model with the router auxiliary loss enabled, this synthesizes a zero scalar instead of requesting/returning the model's actual router aux loss. The replacement compute_loss has no other path that adds that load-balancing term, so such runs silently drop the configured MoE regularizer while reporting/adding zero. Please pass the needed router-loss kwargs through the forward call and return the actual model aux loss, or explicitly keep aux loss disabled.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Went with your second option: aux loss is now explicitly disabled by default. #6938 (merged) sets router_aux_loss_coef to 0 for GRPO on TRL >= 1.7.0, so aux_loss_enabled is False and TRL never adds or logs the term. The zero scalar here is only reached if a user opts in with router_aux_loss_coef > 0, where it keeps the 3-tuple return crash-safe. Dropped the duplicate default from this branch in 7969160.
|
On the MoE TRL 1.7.0 turns the router load-balancing aux loss on by default ( Verified on TRL 1.7.1 (tiny Qwen3-MoE) and TRL 0.25.1:
|
|
@codex review |
1 similar comment
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 796916086f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # aux loss defaults off (router_aux_loss_coef set to 0 in models/rl.py), so | ||
| # compute_aux_loss is normally False -> None. On explicit opt-in the optimized forward | ||
| # cannot compute it, so return a zero placeholder (never None) for TRL's 3-tuple contract. | ||
| aux_loss = logprobs.new_zeros(()) if compute_aux_loss else None |
There was a problem hiding this comment.
Return the real MoE auxiliary loss
With TRL >= 1.7 on MoE models where router_aux_loss_coef is non-zero/default, compute_aux_loss is true and TRL folds this third return value into the policy loss and aux-loss metrics. Returning a fabricated zero here silently disables the router load-balancing penalty for those runs, so expert routing can train differently from the requested configuration; please request/propagate the model's actual aux loss (or explicitly disable the coefficient) instead of returning zero.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Rebased onto current main (8dfd996), which now defaults router_aux_loss_coef to 0 (#6938, merged). So aux_loss_enabled is False by default and TRL never folds in or logs the term; this zero is reached only if a user opts in with router_aux_loss_coef > 0, where it keeps the 3-tuple return crash-safe. That is the explicitly-disable-the-coefficient path you noted.
for more information, see https://pre-commit.ci
rl.py: for trl >= 1.7.0, scope the PEFT removal regex to the ref-adapter block only by anchoring the end on ref_param.data.copy_(param.data), so it no longer also deletes the following gradient-checkpointing enable_input_require_grads() block. Neutralize TRL 1.7.0's `if _is_quantized_model:` bf16 cast the same way the existing is_loaded_in_4bit cast is handled. rl_replacements.py: initialize _extra_moe_kwargs before use (it was referenced before assignment whenever compute_aux_loss was passed) and only request output_router_logits when the aux loss is actually wanted. rl_replacements.py: _get_per_token_logps_and_entropies now returns a 3-tuple (logps, entropies, aux_loss) for trl >= 1.7.0 and a 2-tuple for older TRL, matching how every TRL call site unpacks the result. Without this, TRL 1.7.x _generate_and_score_completions unpacks 3 values from a 2-tuple and raises "not enough values to unpack (expected 3, got 2)".
7969160 to
8dfd996
Compare
|
Rebased this branch onto current
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8dfd996e0d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # aux loss defaults off (router_aux_loss_coef set to 0 in models/rl.py), so | ||
| # compute_aux_loss is normally False -> None. On explicit opt-in the optimized forward | ||
| # cannot compute it, so return a zero placeholder (never None) for TRL's 3-tuple contract. | ||
| aux_loss = logprobs.new_zeros(()) if compute_aux_loss else None |
There was a problem hiding this comment.
Reject explicit router-aux opt-in
When compute_aux_loss is true (i.e. a MoE run explicitly sets a nonzero router_aux_loss_coef), this returns a synthetic zero instead of computing the model's router auxiliary loss. That satisfies TRL's 3-tuple unpacking but silently trains without the requested load-balancing penalty, so experiments that opt in get misleading results; either compute the aux loss or fail fast for this unsupported optimized path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 4785bb4. The optimized GRPO forward cannot produce the router aux loss, so instead of returning a fabricated zero it now fails fast at trainer init when router_aux_loss_coef > 0 (NotImplementedError), and returns None for the aux slot of TRL's 3-tuple otherwise. The default stays at 0 (aux off) via #6938, so the common path is unchanged.
Verified on TRL 1.7.1: default GRPO trains, explicit opt-in raises at construction, SFT/DPO are unaffected, and the injection arity gate still yields a 2-tuple for TRL < 1.7.0.
…ux placeholder Unsloth's optimized GRPO forward cannot compute the MoE router auxiliary loss. Previously an explicit opt-in (router_aux_loss_coef > 0) returned a fabricated zero, silently training without the requested load-balancing penalty. Now reject it at trainer init with a clear NotImplementedError, and return None (not zero) for the aux slot of TRL's 3-tuple. Default stays off (coef 0), so the common path is unaffected.
for more information, see https://pre-commit.ci
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ab5030304a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| with torch.amp.autocast(device_type = "cuda", dtype = self._autocast_dtype): | ||
| if pixel_values is None: | ||
| logits_chunk = unwrapped_model( | ||
| outputs = unwrapped_model( |
There was a problem hiding this comment.
Release hidden-state outputs before log-softmax
When the GRPO hidden-states fallback is active for models without native Unsloth hidden-state support, the wrapper in rl.py requests output_hidden_states=True and returns a ModelOutput that still contains every layer's hidden_states. Binding that full object here keeps those tensors (and any caches) alive while chunked_hidden_states_selective_log_softmax runs; the previous unwrapped_model(...).logits form allowed the ModelOutput to be freed immediately after extracting logits. On large fallback models this can turn old/ref logprob computation into an avoidable OOM; extract outputs.logits and del outputs before the chunked log-softmax (same pattern in the VLM branch below).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch. Fixed in 52c1299: both the text and VLM fallback branches now extract outputs.logits and del outputs before the chunked log-softmax, so the ModelOutput (and its per-layer hidden_states on the output_hidden_states=True path) is freed instead of being held alive during the reduction. Verified default GRPO trains and the opt-in still fails fast at init.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6374c94eb4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # `main` is scheduled/dispatch-only so PR jobs stay fast and a bleeding-edge | ||
| # TRL break does not red every PR; `latest` also runs on PRs to guard our | ||
| # own source-transforms against the current TRL release. | ||
| if: ${{ matrix.channel != 'main' || github.event_name != 'pull_request' }} |
There was a problem hiding this comment.
Move matrix gate out of the job-level if
GitHub's context availability table for jobs.<job_id>.if lists only github, needs, vars, and inputs (while matrix is available on jobs.<job_id>.name and steps.if), so this job-level expression is rejected before the matrix is expanded with an Unrecognized named-value: 'matrix' workflow error. On any PR/scheduled run that loads this workflow, version-compat-ci.yml can fail validation instead of running; move this condition to the steps or express it with matrix exclude/separate jobs.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 170c7b0. matrix is indeed not available in jobs.<job_id>.if, so that expression would fail the whole workflow with Unrecognized named-value: 'matrix'. Dropped the matrix: the job now runs against TRL latest unconditionally and re-runs against TRL main only on schedule/dispatch via a step-level if: ${{ github.event_name != 'pull_request' }} (which uses only the github context). Validated with actionlint.
| sys.path.insert(0, str(_SPOOF_DIR)) | ||
| import _zoo_aggressive_cuda_spoof as _spoof # noqa: E402 | ||
|
|
||
| _spoof.apply() |
There was a problem hiding this comment.
Skip fake-run collection when torch is absent
When daily-fresh-fetch runs, the workflow installs only pytest before collecting tests/version_compat/, but this top-level _spoof.apply() imports torch before the fixture can skip missing runtime dependencies. In that context (and any local version-compat run without torch), collection fails with ModuleNotFoundError: No module named 'torch' instead of skipping this fake-run test; gate the spoof application behind an import check or use a module-level skip when torch is unavailable.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 170c7b0. Added a module-level guard that skips the whole file when torch is absent (if importlib.util.find_spec("torch") is None: pytest.skip(..., allow_module_level=True)) before the spoof import, so daily-fresh-fetch (pytest-only, collects tests/version_compat/) skips it cleanly instead of hitting ModuleNotFoundError at collection.
| # Essentially, for VLMs we do not go via the optimized path in models/, | ||
| # so we don't encounter the Flash Attn left-padding issue. | ||
| logits_chunk = unwrapped_model( | ||
| outputs = unwrapped_model( |
There was a problem hiding this comment.
Forward TRL 1.7 VLM kwargs in logprob replacement
For TRL >= 1.7 multimodal batches that carry the new vision fields such as spatial_shapes, num_tiles, or image_position_ids, upstream GRPO passes those through _get_per_token_logps_and_entropies, but this replacement still calls the model without chunking or forwarding them. Models whose forward depends on those fields will either error or compute logprobs against incomplete vision inputs during GRPO, so include the new VLM kwargs in the chunking and model call when present.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct that TRL 1.7's _get_per_token_logps_and_entropies forwards spatial_shapes / num_tiles / image_position_ids and this replacement does not. Two notes on scope: this is pre-existing (the VLM branch and its _extra_vision_kwargs set are on main, unchanged by this PR), and forwarding these correctly means replicating TRL's per-tile slicing (num_tiles cumsum, image_position_ids[img_start:img_end]) through the chunking loop, which cannot be exercised on the CPU fake-run and needs a real tiled-VLM under GRPO to validate. It only affects VLM architectures that require those fields (the common pixel_values + image_grid_thw path is already forwarded). Keeping this PR focused on the TRL >= 1.7 crash/arity/PEFT fixes and tracking the VLM field forwarding as a separate change so it can be validated properly rather than shipped blind.
- Drop the invalid job-level matrix if (matrix is not available in jobs.<id>.if -> 'Unrecognized named-value: matrix' fails the whole workflow). Use a single job that runs vs TRL latest always and re-runs vs TRL main only on schedule/dispatch via a step-level github.event_name guard. Validated with actionlint. - Module-level skip the fake-run test when torch is absent so daily-fresh-fetch (pytest-only, collects tests/version_compat/) does not crash on the top-level spoof import.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 170c7b02f1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if os.environ.get("UNSLOTH_FORCE_FLOAT32", "0") == "1": | ||
| self._autocast_dtype = torch.float16 | ||
|
|
||
| compute_aux_loss = kwargs.get("compute_aux_loss", None) |
There was a problem hiding this comment.
Thread the new TRL 1.7 VLM kwargs through logprob forwards
For TRL >= 1.7 this method is called with additional multimodal kwargs such as spatial_shapes, num_tiles, and image_position_ids, and _generate_and_score_completions stores them in inputs; the new 1.7 handling only pulls compute_aux_loss, while the chunking code and model calls below still drop those fields. In GRPO on LFM2-VL or image-position based VLMs, the patched trainer will omit required vision inputs when recomputing logprobs, causing failures or incorrect logprobs even though the new fake-run only covers text/older VLM kwargs. Please propagate these kwargs through the chunk slicing and _extra_vision_kwargs alongside this 1.7 update.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Agreed this is a real gap for LFM2-VL / image-position VLMs on TRL 1.7. It is pre-existing (the VLM branch and its kwarg set are unchanged by this PR) and forwarding these correctly means replicating TRL's per-tile / per-image slicing (num_tiles cumsum for spatial_shapes, image_position_ids[img_start:img_end]) through both chunk-construction paths, which the CPU fake-run cannot exercise and which needs a real tiled VLM under GRPO to validate. Rather than ship that blind alongside the crash/arity fix here, I opened #6960 to track it so it can get a proper logprob-parity check on GPU. Keeping this PR scoped to the TRL >= 1.7 crash/arity/PEFT fixes.
| import unsloth # noqa: F401 -- _gpu_init bootstrap under spoof | ||
| except Exception as e: # pragma: no cover - env issue, not a regression | ||
| pytest.skip(f"`import unsloth` failed under spoof: {type(e).__name__}: {str(e)[:200]}") |
There was a problem hiding this comment.
Fail the fake-run when unsloth import breaks
In the new grpo-fake-run CI job the environment has just installed unsloth, trl, and their runtime deps, so an exception from import unsloth is exactly the import-time TRL/transformers drift this canary is meant to catch. Catching every exception and converting it to pytest.skip lets the job go green with skipped tests instead of failing when the patcher cannot even import, masking the regressions this workflow was added to detect; only known missing-dependency cases should skip.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in f2870e6. Dropped the broad except Exception -> pytest.skip around import unsloth (and the SFT/DPO helper): the grpo-fake-run job installs unsloth/trl/deps, so an import failure is the import-time drift this canary must fail on. Only the find_spec not-installed checks skip now.
unsloth/trl are installed in the grpo-fake-run job, so a failing import is the import-time drift this canary must catch. Keep only the not-installed find_spec skips; let a real import error fail the test.
for more information, see https://pre-commit.ci
|
@codex review |
1 similar comment
|
@codex review |
|
Codex Review: Didn't find any major issues. Breezy! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
The TRL < 1.7.0 per-token-logps return downgrade was an exact-string replace anchored on the full return line incl. its comment, so a reformat (e.g. pre-commit) could silently no-op it and ship a 3-tuple to older TRL. Switch to a regex tolerant of comment/whitespace drift, and raise if the anchor stops matching (re.subn count != 1) instead of failing silently. Add a monkeypatched trl_version unit test asserting both arities, since CI only installs TRL >= 1.7.0 and never exercised the downgrade otherwise.
for more information, see https://pre-commit.ci
|
@codex review |
|
@codex review |
|
@codex review |
The SFT/DPO fake patch runs only checked the generated trainer parses. Also assert the shared QLoRA _is_quantized_model bf16 cast is neutralized (TRL 1.7's spelling, present in both sft_trainer and dpo_trainer), so a structural TRL change to that block is caught for SFT/DPO too, not just GRPO.
for more information, see https://pre-commit.ci
|
@codex review |
|
Codex Review: Didn't find any major issues. Delightful! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
The elif is_peft_model(model) and args.beta != 0.0: ref-adapter block was introduced in TRL 1.4.0 and is unchanged through 1.7.x, but the removal was gated at >= 1.7.0, so for 1.4 <= TRL < 1.7 the transform fell through to the 0.27 branch (which matches the older if is_peft_available()... form) and silently no-oped: a PEFT + beta != 0 GRPO run then computed the KL reference from the copied ref adapter instead of the base model. Lower the gate to 1.4.0 and keep the 1.7.0-only router aux-loss fail-fast nested. Widen the pinned-symbol contract test to run from 1.4.0 so the covered versions are actually exercised.
for more information, see https://pre-commit.ci
|
@codex review |
|
Codex Review: Didn't find any major issues. You're on a roll. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
danielhanchen
left a comment
There was a problem hiding this comment.
Verified the TRL >= 1.7.0 GRPO transforms against real TRL 1.7.1 and the version-compat contract tests; the PEFT ref-adapter removal, QLoRA cast neutralization, and 3-tuple arity gate are correct. Also lowered the PEFT-removal gate to the TRL 1.4.0 floor where the elif block first appeared. Fake-run and CPU fake-train validated green.