Skip to content

fix(#1221): Transformer.Predict bypassed eval-mode wrapper, Dropout fired at inference - #1242

Merged
ooples merged 3 commits into
masterfrom
fix/1221-transformer-uniform-output
May 4, 2026
Merged

ooples merged 3 commits into
masterfrom
fix/1221-transformer-uniform-output

Conversation

@ooples

@ooples ooples commented May 3, 2026 •

Copy link
Copy Markdown
Owner

Closes #1221.

Summary

Root cause for #1221's "Transformer training completes but eval logits are uniform-over-V" reproducer: Transformer<T>.Predict() overrode NeuralNetworkBase.Predict() directly, bypassing the wrappers introduced in 6bf657c45:

  • SetTrainingMode(false) for the forward pass
  • NoGradScope to suppress autograd
  • NormalizeInputBatchDim
  • Output squeeze on unbatched inputs

For a Phase A4 byte-LM config (V=256, d=64, L=2, ctx=32) the layer chain contains ~5 DropoutLayer instances. With training mode left on at inference (which happened any time a caller re-enabled training between train and eval), Dropout's stochastic mask reroll fired on every Predict call, pushing the trained output Dense into a configuration where eval logits collapse to uniform-over-V (NLL = ln(V), top-1 = 0%).

Fix

Move the special-case layer routing (DecoderLayer cross-attention, AttentionLayer mask, MultiHeadAttention encoder-output tracking) from Predict() to PredictEager(). The base Predict() now wraps the override with its standard inference scaffolding — eval mode, no-grad, batch promote, output squeeze — and Dropout becomes a no-op at inference automatically.

Test plan

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Prediction now preserves evaluation-mode semantics so Dropout and training-mode toggles behave consistently during inference, yielding stable outputs.
    • Improved encoder–decoder attention routing and attention-mask defaulting to preserve correct cross-attention behavior.
  • Tests

    • Added an integration regression test that verifies deterministic predictions when the model is left in training mode and confirms training-mode is preserved.

…so eval-mode wrapper applies

Root cause: Transformer<T>.Predict() overrode NeuralNetworkBase.Predict()
directly, which meant it bypassed the base's wrappers introduced in
6bf657c (fix(Predict+Dense)):
  - SetTrainingMode(false) for the duration of the forward pass
  - NoGradScope to suppress autograd recording
  - NormalizeInputBatchDim for unbatched inputs
  - Output squeeze when input was promoted

For a token-input Transformer (Phase A4 byte-LM config: V=256, d=64,
L=2, ctx=32) the layer chain contains ~5 DropoutLayer instances (one
post-embedding, two per encoder block). With training mode left on at
inference time — which happened in any caller that didn't manually
call SetTrainingMode(false) before Predict, including any code path
that re-enabled training between train and eval — Dropout's stochastic
mask reroll fired on every Predict call. After many training steps the
trained Dense output projection settled into a configuration where the
random Dropout masks at eval time pushed logits toward uniform-over-V
(eval NLL = ln(V) exactly, top-1 = 0%).

Fix: Transformer's special-case routing (DecoderLayer cross-attention,
AttentionLayer mask, MultiHeadAttention encoder-output tracking) is
moved from Predict() to PredictEager(). The base's Predict() now wraps
the override with its standard inference scaffolding — eval mode,
no-grad, batch promote, output squeeze — and Dropout becomes a no-op
at inference automatically.

Test: Predict_WithExternalTrainingModeOn_StillDeterministic forces
training mode ON between Train and Predict, then verifies two Predict
calls on the same input produce bit-identical logits. Pre-fix this
test fails with max abs diff = 9.84e-3 (Dropout firing); post-fix
diff < 1e-6 (Dropout properly disabled).

Verification: broader Transformer test set went from 412 → 370 failures
(42 tests recovered, no regressions). Remaining 370 are pre-existing
#1224 cluster failures unrelated to this fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented May 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
aidotnet_website Ignored Ignored May 3, 2026 11:41pm
aidotnet-playground-api Ignored Ignored Preview May 3, 2026 11:41pm

Copilot AI review requested due to automatic review settings May 3, 2026 22:38
@coderabbitai

coderabbitai Bot commented May 3, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 274fd6e0-0c50-4e30-ac1e-28e43905654a

📥 Commits

Reviewing files that changed from the base of the PR and between b752f56 and 7d19cec.

📒 Files selected for processing (2)
  • src/NeuralNetworks/Transformer.cs
  • tests/AiDotNet.Tests/IntegrationTests/NeuralNetworks/TransformerProductionScaleConvergenceIssue1221Tests.cs

Walkthrough

Transformer moves its forward-pass override from the public Predict to protected override PredictEager, delegating inference scaffolding (training-mode toggle, no-grad scope, batch promotion, output squeeze) to NeuralNetworkBase<T>. Eager forward attempts a GPU fast path or runs layers sequentially, capturing encoder output for decoder cross-attention only when a decoder exists. A regression test ensures deterministic inference when training mode is externally left enabled.

Changes

Transformer Prediction Architecture

Layer / File(s) Summary
Method Override Relocation
src/NeuralNetworks/Transformer.cs
Replaced public override Tensor<T> Predict(Tensor<T> input) with protected override Tensor<T> PredictEager(Tensor<T> input).
Eager Forward / Control Flow
src/NeuralNetworks/Transformer.cs
Eager implementation: try TryForwardGpuOptimized; otherwise initialize output, compute mask (use AttentionMask or default all-ones), iterate Layers and route calls: DecoderLayer.Forward(output, encoderOutput ?? output, mask), AttentionLayer.Forward(output, mask), and ILayer<T>.Forward(output) for other layers.
Encoder/Decoder Cross-Attention Capture
src/NeuralNetworks/Transformer.cs
encoderOutput is updated on each MultiHeadAttentionLayer<T> encountered until the first DecoderLayer<T> is seen (tracked via seenDecoder) and the captured encoder context is retained only if a decoder exists downstream.
Regression Test / API usage
tests/AiDotNet.Tests/.../TransformerProductionScaleConvergenceIssue1221Tests.cs
Added Predict_WithExternalTrainingModeOn_StillDeterministic which builds a Transformer (dropoutRate param added to builder), validates presence of DropoutLayer, trains briefly, forces SetTrainingMode(true) before inference, calls Predict twice, asserts outputs match within max-abs-diff < 1e-3, and asserts model.IsTrainingMode remains true after Predict.
Test Helper signature
tests/.../TransformerProductionScaleConvergenceIssue1221Tests.cs
BuildTransformer extended to accept double dropoutRate = 0.1 and pass it into TransformerArchitecture.

Sequence Diagram(s)

(omitted — changes are internal control-flow adjustments limited to transformer forward-pass and a unit/integration test; conditions for diagram generation not met)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Blocking code-quality notes

  • If any TODOs, stubs, or simplified implementations remain in Transformer.cs (especially around GPU fast-path or attention mask defaults), treat them as blocking until production-ready implementations are provided.
  • Ensure all layers’ Forward contracts (signatures and null/empty-mask semantics) are documented and validated; mismatches here can silently break training/eval parity (blocking).
  • Confirm TryForwardGpuOptimized has a robust fallback and well-tested correctness on all backends; any backend-specific conditional code is a potential blocker until tested.

Poem

A forward moved down a careful lane,
Base wraps inference, keeping state the same.
Dropout stays hidden when it should not play,
Two calls match — the noise kept away. 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically identifies the core fix: moving the Predict override to PredictEager and restoring base-class eval-mode wrapping to prevent Dropout at inference.
Linked Issues check ✅ Passed The code changes directly address #1221 requirements: restoring the base Predict wrapper to enforce eval mode (disabling Dropout), protecting against training-mode leakage, and adding a regression test.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the Predict eval-mode bypass and implementing the encoder-output tracking logic for decoder cross-attention per the linked issue.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1221-transformer-uniform-output

Review rate limit: 2/5 reviews remaining, refill in 33 minutes and 31 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Fixes #1221 where Transformer<T>.Predict() bypassed base inference wrappers (eval-mode toggle/no-grad/batch normalization), causing Dropout to run during inference and produce stochastic/uniform logits.

Changes:

  • Moves Transformer’s special-case forward routing from Predict() into PredictEager() so base Predict() wrappers are applied.
  • Adds a regression test ensuring Predict() remains deterministic even if callers force training mode on before inference.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/NeuralNetworks/Transformer.cs Routes Transformer’s eager forward via PredictEager() to preserve NeuralNetworkBase.Predict() inference scaffolding (fixes Dropout-at-inference).
tests/AiDotNet.Tests/IntegrationTests/NeuralNetworks/TransformerProductionScaleConvergenceIssue1221Tests.cs Adds regression test covering the “external training mode re-enabled before Predict” scenario.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/NeuralNetworks/Transformer.cs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@tests/AiDotNet.Tests/IntegrationTests/NeuralNetworks/TransformerProductionScaleConvergenceIssue1221Tests.cs`:
- Around line 405-436: Add an assertion that Predict restores the caller's
training-mode state: after the two Predict calls (which occur after
model.SetTrainingMode(true) and the existing checks), add
Assert.True(model.IsTrainingMode) to verify the model's training-mode flag was
preserved; reference the existing methods model.SetTrainingMode(true),
model.Predict(...), and model.IsTrainingMode to locate where to insert this
post-call check so the test fails if Transformer.Predict or any override fails
to restore the prior mode.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ad610159-540b-4405-b2be-d6bd7bb3f8c1

📥 Commits

Reviewing files that changed from the base of the PR and between 7785696 and e69c9b4.

📒 Files selected for processing (2)
  • src/NeuralNetworks/Transformer.cs
  • tests/AiDotNet.Tests/IntegrationTests/NeuralNetworks/TransformerProductionScaleConvergenceIssue1221Tests.cs

…ger doc

- TransformerProductionScaleConvergenceIssue1221Tests:
  Predict_WithExternalTrainingModeOn_StillDeterministic now asserts
  the post-Predict state restores caller's IsTrainingMode (locks in
  the save/restore contract). Tolerance loosened from 1e-6 to 1e-3
  so GPU floating-point nondeterminism (kernel scheduling /
  reduction-order variance) doesn't make the test flaky on CI agents
  with GPU-backed Engine. Pre-fix Dropout-fired diff is ~1e-2 so
  1e-3 still cleanly distinguishes the bug from float noise.
- Transformer.cs PredictEager remarks scoped to encoder/decoder
  routing only — the wrapper-level "why" (training mode toggle,
  no-grad, batch-promote) belongs on Predict's docs, not on this
  protected override. Cross-references back to Predict for the
  inference-scaffolding contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/NeuralNetworks/Transformer.cs`:
- Around line 475-490: PredictEager currently captures encoderOutput only once
(when null) which records the first MultiHeadAttentionLayer<T> output; change
the logic to capture the last such encoder-side attention output before any
decoder layer by removing the "only-if-null" guard and instead track whether a
DecoderLayer<T> has been seen (e.g., bool decoderSeen = false), set decoderSeen
= true when you hit a DecoderLayer<T>, and whenever you encounter a
MultiHeadAttentionLayer<T> and decoderSeen is false assign/overwrite
encoderOutput with that layer's output so the final encoder block's attention
output is retained for cross-attention.

In
`@tests/AiDotNet.Tests/IntegrationTests/NeuralNetworks/TransformerProductionScaleConvergenceIssue1221Tests.cs`:
- Around line 387-455: The test relies on BuildTransformer defaults for dropout;
make the dropout precondition explicit by passing a non-zero dropout rate into
the BuildTransformer call (e.g., dropoutRate: 0.1) or, alternatively, assert
that the constructed model contains an enabled Dropout layer before using
pred1/pred2; update the test's model construction line (the BuildTransformer
invocation) or add a pre-comparison check that inspects the model's layers for a
Dropout instance with DropoutRate > 0 to guarantee the regression is actually
exercised (this targets the BuildTransformer usage and the Transformer.Predict
path referenced in the comment).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8a36bdea-5620-4015-81fd-f6fe6d5b9e3e

📥 Commits

Reviewing files that changed from the base of the PR and between e69c9b4 and b752f56.

📒 Files selected for processing (2)
  • src/NeuralNetworks/Transformer.cs
  • tests/AiDotNet.Tests/IntegrationTests/NeuralNetworks/TransformerProductionScaleConvergenceIssue1221Tests.cs

Comment thread src/NeuralNetworks/Transformer.cs
…t in determinism test

Addresses two reviewer threads on PR #1242:

OVkn — Transformer.PredictEager was capturing the FIRST encoder
MultiHeadAttention output and freezing it, so a multi-block encoder
stack always fed the decoder the first block's output instead of the
fully-encoded representation. Replace the once-only `encoderOutput is
null` check with a `seenDecoder` flag: keep updating encoderOutput on
every encoder MultiHead until the first decoder is reached, then
freeze.

OVkt — Predict_WithExternalTrainingModeOn_StillDeterministic relied
on TransformerArchitecture's framework-default dropoutRate (0.1) to
produce stochastic Predict output pre-fix. If that default ever drops
to 0.0 the test silently no-ops. Pin dropoutRate=0.2 explicitly via a
new BuildTransformer parameter, and add a precondition assertion that
the model contains at least one DropoutLayer with SupportsTraining=true
before comparing pred1/pred2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 3, 2026 23:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@ooples
ooples merged commit 8bc100a into master May 4, 2026
36 of 49 checks passed
@ooples
ooples deleted the fix/1221-transformer-uniform-output branch May 4, 2026 00:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transformer<T>.Train() produces uniform output at production scale (v0.166.0) — top-1=0% / PPL=|V| on 1MB byte-LM

3 participants