Repository navigation
fix: deserialize weight-preservation (#1465) + paper-faithful CRF/Donut/NER fixes - #1466
Conversation
34 Video / NER / OCR / TTS models called `Layers.Clear(); InitializeLayers()`
(or `ClearLayers(); InitializeLayers()`) inside DeserializeNetworkSpecificData.
The base NeuralNetworkBase.DeserializeInternalUnchecked already recreates every
layer with its saved weights BEFORE that override runs, so re-initializing
discarded the deserialized weights and left the model randomly initialized —
breaking clone (DeepCopy) and load-from-disk parity.
Fix: drop the clear+re-initialize for native-mode layers (the base already
populated Layers); keep only the ONNX-session rebuild where applicable. For
the model that caches per-component layer references used in its forward
(OpenSora), re-point those cached references at the freshly deserialized
Layers via its existing ExtractLayerReferences instead of rebuilding.
Verified weights are preserved bit-identically through a Serialize ->
Deserialize round-trip (param max diff = 0) on representatives of every base
pattern: VFIMamba (VideoNeuralNetworkBase), CRNN (OCR), FastDVDNet
(unconditional ClearLayers), MARS5TTS (TTS transformer), and OpenSora
(ref-caching DiT/VAE).
Out of scope (separate pre-existing bugs, NOT the re-randomization addressed
here), left as follow-ups:
- Donut: removing the re-init exposes a SwinTransformerBlockLayer.SetParameters
round-trip throw (the block does not restore _dim/_mlpRatio from serialized
metadata). Reverted to the rebuild-from-scratch path with an explanatory
note so deserialize does not throw; needs the layer fix first.
- LSTMCRF (and possibly other NER models): one layer does not round-trip
through the base serializer, so a residual weight gap remains after this fix
(improvement, no throw).
- SlowFast already rebuilt its mirror views correctly and was left untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughRemove destructive native-mode post-deserialization re-initialization (Layers.Clear()/InitializeLayers()) across many models so base-deserialized layer instances and trained weights are preserved; rebuild mirror/group references where necessary and persist/restore out-of-layer tensors for Donut. ChangesDeserialization Weight Preservation Across Models
Estimated code review effort: Possibly related PRs
Suggested labels:
Blocking notes (production readiness)
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/Document/PixelToSequence/Donut.cs (1)
1184-1203:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winBlocking: native deserialization still discards restored weights.
Line 1193 onward still clears/rebuilds layers in native mode, so deserialized parameters are thrown away and the model is re-randomized. This must not ship silently; either preserve loaded layers or fail fast until the Swin serialization contract is fixed.
💡 Concrete fix (fail fast until layer serialization is fixed)
- Layers.Clear(); - _patchEmbeddingLayers.Clear(); - _encoderLayers.Clear(); - _decoderEmbeddingLayers.Clear(); - _decoderLayers.Clear(); - _outputLayers.Clear(); - - if (_useNativeMode) - { - InitializeLayers(); - } + if (_useNativeMode) + { + throw new InvalidOperationException( + "Donut native deserialization cannot safely proceed: SwinTransformerBlockLayer does not round-trip parameter counts yet. " + + "Fix layer serialization and then reuse deserialized Layers without re-initialization."); + } + + Layers.Clear(); + _patchEmbeddingLayers.Clear(); + _encoderLayers.Clear(); + _decoderEmbeddingLayers.Clear(); + _decoderLayers.Clear(); + _outputLayers.Clear(); InitializeEmbeddings();As per coding guidelines, “Every PR must contain production-ready code” and “Incomplete features… where some code paths work but others silently do nothing” are blocking.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/Document/PixelToSequence/Donut.cs` around lines 1184 - 1203, The current logic in DeserializeInternalUnchecked clears reconstructed native-mode layers (Layers.Clear(), _patchEmbeddingLayers.Clear(), _encoderLayers.Clear(), _decoderEmbeddingLayers.Clear(), _decoderLayers.Clear(), _outputLayers.Clear()) and then calls InitializeLayers() when _useNativeMode is true, discarding deserialized weights; change this to fail fast: detect _useNativeMode after deserialization and throw a clear exception (e.g., InvalidOperationException) explaining that native-mode layer-level deserialization is unsupported until the SwinTransformerBlockLayer serialization contract is fixed, instead of clearing/reinitializing or silently re-randomizing; leave the non-native path unchanged so managed-mode continues to work.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/Video/Enhancement/Upscale4KAgent.cs`:
- Around line 216-220: In Upscale4KAgent<T>.DeserializeNetworkSpecificData,
dispose any existing OnnxModel before overwriting it: call OnnxModel?.Dispose()
immediately before assigning OnnxModel = new OnnxModel<T>(...), so the previous
ONNX session/resources are released (CreateNewInstance() may have already set
OnnxModel; final disposal remains handled by VideoNeuralNetworkBase<T>.Dispose).
In `@src/Video/Enhancement/VideoGigaGAN.cs`:
- Around line 228-232: DeserializeNetworkSpecificData is recreating OnnxModel
without disposing the prior instance, leaking the ONNX session; modify
VideoGigaGAN<T>::DeserializeNetworkSpecificData to check if OnnxModel is
non-null and call Dispose() (or Dispose(bool) appropriately) on the existing
OnnxModel before assigning a new OnnxModel(p, _options.OnnxOptions), and ensure
Dispose(bool) still correctly disposes the final OnnxModel instance to avoid
double-dispose issues.
In `@src/Video/FrameInterpolation/ABME.cs`:
- Around line 230-234: The deserializer overwrites the existing OnnxModel
without disposing its ONNX session, leaking native resources; before assigning a
new OnnxModel in the ABME class (the branch where !_useNativeMode &&
_options.ModelPath is set and OnnxModel = new OnnxModel<T>(...)), explicitly
dispose the current OnnxModel/session (e.g., call its Dispose/Close/Release
method or a DisposeSession helper) if OnnxModel is non-null, then assign the new
instance so the previous native ONNX session is released.
In `@src/Video/FrameInterpolation/AMT.cs`:
- Around line 213-217: The code replaces the OnnxModel field without disposing
the previous ONNX session, leaking native resources; before assigning a new
OnnxModel<T>(p, _options.OnnxOptions) in the AMT class (the block guarded by
_useNativeMode and _options.ModelPath), check if the existing OnnxModel is
non-null and dispose it (or call its Dispose/Close method) then set the new
instance, ensuring null-safety and preserving exception safety (dispose in a
try/finally or dispose before new allocation) so repeated deserialize/deep-copy
flows do not leak ONNX resources.
In `@src/Video/FrameInterpolation/BiMVFI.cs`:
- Around line 222-226: The current deserialize path replaces the OnnxModel field
without disposing the previous native ONNX session, leaking resources; before
assigning a new OnnxModel in the branch that checks !_useNativeMode and
_options.ModelPath, call Dispose (or the appropriate cleanup method) on the
existing OnnxModel/session instance (if non-null) and then null it out before
constructing the new OnnxModel<T>(p, _options.OnnxOptions) so the previous
native session is released.
In `@src/Video/FrameInterpolation/GIMMVFI.cs`:
- Around line 219-223: When replacing the OnnxModel in GIMMVFI during
deserialization/clone, first dispose the prior ONNX resources to avoid native
leaks: detect the existing OnnxModel (or its underlying session) and call its
Dispose/DisposeSession method (or dispose the session field) before creating and
assigning new OnnxModel<T>(p, _options.OnnxOptions); ensure this check runs in
the same branch that uses _useNativeMode and _options.ModelPath so the old
session is cleaned up only when being replaced.
In `@src/Video/FrameInterpolation/IFRNet.cs`:
- Around line 213-217: Before assigning a new OnnxModel in the block that checks
_useNativeMode and _options.ModelPath, dispose the existing ONNX session to
avoid leaking native resources: locate the assignment to OnnxModel in the IFRNet
override, call the existing session's disposal/cleanup (e.g.,
OnnxModel?.Dispose() or its equivalent close method) before creating the new
OnnxModel<T>(p, _options.OnnxOptions), and ensure this disposal is safe if
OnnxModel is null and does not swallow needed exceptions.
---
Outside diff comments:
In `@src/Document/PixelToSequence/Donut.cs`:
- Around line 1184-1203: The current logic in DeserializeInternalUnchecked
clears reconstructed native-mode layers (Layers.Clear(),
_patchEmbeddingLayers.Clear(), _encoderLayers.Clear(),
_decoderEmbeddingLayers.Clear(), _decoderLayers.Clear(), _outputLayers.Clear())
and then calls InitializeLayers() when _useNativeMode is true, discarding
deserialized weights; change this to fail fast: detect _useNativeMode after
deserialization and throw a clear exception (e.g., InvalidOperationException)
explaining that native-mode layer-level deserialization is unsupported until the
SwinTransformerBlockLayer serialization contract is fixed, instead of
clearing/reinitializing or silently re-randomizing; leave the non-native path
unchanged so managed-mode continues to work.
🪄 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: 9d2dbd62-6457-463e-ae2f-832077fb8445
📒 Files selected for processing (35)
src/Document/OCR/TextRecognition/CRNN.cssrc/Document/PixelToSequence/Donut.cssrc/NER/SequenceLabeling/LSTMCRF.cssrc/NER/SpanBased/SpanBasedNERBase.cssrc/NER/TransformerBased/TransformerNERBase.cssrc/TextToSpeech/CodecBased/MARS5TTS.cssrc/TextToSpeech/FlowDiffusion/F5TTS.cssrc/Video/Denoising/FastDVDNet.cssrc/Video/Enhancement/BasicVSR.cssrc/Video/Enhancement/DOVE.cssrc/Video/Enhancement/DualXVSR.cssrc/Video/Enhancement/FlashVSR.cssrc/Video/Enhancement/MIAVSR.cssrc/Video/Enhancement/RealBasicVSR.cssrc/Video/Enhancement/RealisVSR.cssrc/Video/Enhancement/SeedVR.cssrc/Video/Enhancement/StableVideoSR.cssrc/Video/Enhancement/Upscale4KAgent.cssrc/Video/Enhancement/VideoGigaGAN.cssrc/Video/FrameInterpolation/ABME.cssrc/Video/FrameInterpolation/AMT.cssrc/Video/FrameInterpolation/BiMVFI.cssrc/Video/FrameInterpolation/DRVI.cssrc/Video/FrameInterpolation/DynamiCrafter.cssrc/Video/FrameInterpolation/EMAVFI.cssrc/Video/FrameInterpolation/GIMMVFI.cssrc/Video/FrameInterpolation/IFRNet.cssrc/Video/FrameInterpolation/MoG.cssrc/Video/FrameInterpolation/MoMo.cssrc/Video/FrameInterpolation/STMFNet.cssrc/Video/FrameInterpolation/TLBVFI.cssrc/Video/FrameInterpolation/ToonCrafter.cssrc/Video/FrameInterpolation/VFIMamba.cssrc/Video/Generation/CogVideo.cssrc/Video/Generation/OpenSora.cs
…1465 fix) Replaces the earlier "blocked/reverted" Donut workaround with the actual root- cause fixes. Three real bugs, fixed properly instead of documented: 1. SwinTransformerBlockLayer did not emit its constructor settings (numHeads, windowSize, shiftSize, mlpRatio) from GetMetadata, so the deserialization reflection-matcher rebuilt each block with default head/window/MLP settings. The relative-position-bias table is sized from (2*windowSize-1)^2 * numHeads, so a wrong windowSize/numHeads gave a different ParameterCount than was serialized and SetParameters threw. Now emits Dim/NumHeads/WindowSize/ ShiftSize/MlpRatio. 2. SwinPatchEmbeddingLayer likewise did not emit PatchSize/EmbedDim, so the matcher rebuilt the patch-projection conv with a default embedding dim (e.g. 64 instead of 16 -> 3136 params vs 816). The buffered _pendingParameters replay on first forward then threw on the size mismatch. Now emits PatchSize/EmbedDim. 3. Donut (PixelToSequence) DeserializeNetworkSpecificData cleared Layers and re-ran InitializeLayers (re-randomizing), and InitializeEmbeddings re- randomized the network-level token/position embeddings (which live outside Layers and are trained via the custom gradient path, so the base layer serialization never covered them). Now: re-derive the per-group mirror lists from the deserialized Layers (no re-randomize), and serialize+restore the embeddings (with a backward-compatible read for models saved before this). Verified: a small Donut Predict -> Clone -> Predict produces bit-identical output (max diff 0); all four Swin transformer blocks + the patch embedding round-trip with parameter max diff 0. LSTMCRF Clone_ShouldProduceIdenticalOutput also passes (its earlier "residual" was a diagnostic artifact from serializing before the lazy layers were resolved by a forward). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/Document/PixelToSequence/Donut.cs`:
- Around line 1200-1215: In WriteOptionalTensor, replace the generic numeric
conversion Convert.ToDouble(span[i]) with the library helper
NumOps.ToDouble(span[i]) so writing matches ReadOptionalTensor's
NumOps.FromDouble and the rest of the codebase; update any using/imports if
necessary so NumOps is in scope and keep the surrounding logic in
WriteOptionalTensor (handling tensor null, rank, shape, and iterating
tensor.Data.Span) unchanged.
🪄 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: ac10d415-8cc1-451c-a9d8-ed243685e837
📒 Files selected for processing (3)
src/Document/PixelToSequence/Donut.cssrc/NeuralNetworks/Layers/SwinPatchEmbeddingLayer.cssrc/NeuralNetworks/Layers/SwinTransformerBlockLayer.cs
Fixes the two remaining clone failures the deserialize work surfaced, at the root, and makes the models match their papers. ConditionalRandomFieldLayer (Lample et al. 2016): a CRF's parameters (transition matrix [C,C], start/end scores [C]) are independent of sequence length, so it must run Viterbi / forward-backward over whatever length the input provides. Removed the fixed-_sequenceLength equality throw (CPU and GPU paths) and made the decode buffers/loops size from the actual per-call length. A single trained CRF now labels sequences of any length. LSTMCRF / BiLSTMCRF / CNNBiLSTMCRF PreprocessTokens: stop padding every input UP to MaxSequenceLength. That made a short [8, embDim] sequence emit a [256, numLabels] output that no longer matched its [8, numLabels] gold labels, and trained the CRF NLL over a mostly-padding sequence (no mask) — slow and divergent. MaxSequenceLength is now treated as an upper bound (truncate only); padding to a common length is a batching concern for the caller. LSTMCRF.Train: route through the shared CRF-aware step (RunCrfAwareTrainStep, the same path BiLSTMCRF/CNNBiLSTMCRF use) so it trains the CRF negative log-likelihood (the paper objective) instead of per-token cross-entropy on raw emissions, and preprocesses tokens + labels to a consistent length. VisionLanguage Donut (Kim et al. 2022): its Swin encoder must tokenize the raw [B,3,H,W] image via a patch embedding before any attention block — without it a raw image flowed into the first MHA whose Q/K/V expect visionDim, throwing "embedding dimension N != weight dimension visionDim". Added the patch embedding to CreateDefaultDocumentOCRLayers (mirrors the OmniGen2 fix) and updated the encoder/decoder boundary. Verified: LSTMCRF/BiLSTMCRF/CNNBiLSTMCRF model-family suites pass (incl. Clone, Clone_AfterTraining, MoreData, GradientFlow); Donut Clone_ShouldProduceIdenticalOutput passes. Donut's training-invariant tests now run the real architecture but exceed the 120s CPU budget at the paper-scale default config (VisionDim=1024, 16 layers) — a perf item tracked separately, not a correctness regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/NER/SequenceLabeling/LSTMCRF.cs (1)
197-241:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winBLOCKING:
TrainAsyncwas not updated to useRunCrfAwareTrainStep, unlikeTrain()and sibling models.The synchronous
Train()method (line 327-335) was correctly updated to delegate toRunCrfAwareTrainStep, butTrainAsyncstill uses the old pattern:
- Direct
LossFunction.CalculateLosson Viterbi-decoded output (non-differentiable)- Creates an unused gradient tensor
gt(line 222-223) with a comment about removed backward pass- Inconsistent with
BiLSTMCRF.TrainAsyncandCNNBiLSTMCRF.TrainAsyncwhich both useRunCrfAwareTrainStepThis means async training optimizes the wrong objective (per-token cross-entropy on argmax labels) instead of the CRF negative log-likelihood.
🐛 Proposed fix to align with sibling models
return Task.Run(() => { for (int epoch = 1; epoch <= epochs; epoch++) { cancellationToken.ThrowIfCancellationRequested(); - SetTrainingMode(true); - try - { - var preprocessed = PreprocessTokens(tokenEmbeddings); - var preprocessedLabels = PreprocessLabels(labels, preprocessed.Shape[0]); - var output = Forward(preprocessed); - double loss = NumOps.ToDouble(LossFunction.CalculateLoss( - output.ToVector(), preprocessedLabels.ToVector())); - var grad = LossFunction.CalculateDerivative(output.ToVector(), preprocessedLabels.ToVector()); - var gt = Tensor<T>.FromVector(grad); - // Backward removed — tape-based training handles gradients - _optimizer.UpdateParameters(Layers); - - progress?.Report(new NERTrainingProgress - { - CurrentEpoch = epoch, - TotalEpochs = epochs, - CurrentBatch = 1, - TotalBatches = 1, - Loss = loss - }); - } - finally - { - SetTrainingMode(false); - } + // Shared CRF-aware step — see BiLSTMCRF.TrainAsync for the + // longer-form rationale (routes through CRF NLL when UseCRF=true). + double loss = RunCrfAwareTrainStep( + tokenEmbeddings, labels, _options.UseCRF, _optimizer); + + progress?.Report(new NERTrainingProgress + { + CurrentEpoch = epoch, + TotalEpochs = epochs, + CurrentBatch = 1, + TotalBatches = 1, + Loss = loss + }); } }, cancellationToken);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/NER/SequenceLabeling/LSTMCRF.cs` around lines 197 - 241, TrainAsync is still using the old non-differentiable loss path (calling LossFunction.CalculateLoss on Viterbi-decoded output and creating a dead gradient tensor) instead of delegating to the CRF-aware training step; update SequenceLabeling.LSTMCRF.TrainAsync to mirror Train() and the sibling implementations (BiLSTMCRF.TrainAsync, CNNBiLSTMCRF.TrainAsync) by calling RunCrfAwareTrainStep inside the Task.Run loop (passing the preprocessed inputs from PreprocessTokens/PreprocessLabels, the optimizer, Layers, and reporting progress), remove the unused LossFunction.CalculateLoss/LossFunction.CalculateDerivative and the dummy gt tensor, and keep SetTrainingMode(true/false) and _optimizer.UpdateParameters usage consistent with RunCrfAwareTrainStep integration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/NER/SequenceLabeling/LSTMCRF.cs`:
- Around line 197-241: TrainAsync is still using the old non-differentiable loss
path (calling LossFunction.CalculateLoss on Viterbi-decoded output and creating
a dead gradient tensor) instead of delegating to the CRF-aware training step;
update SequenceLabeling.LSTMCRF.TrainAsync to mirror Train() and the sibling
implementations (BiLSTMCRF.TrainAsync, CNNBiLSTMCRF.TrainAsync) by calling
RunCrfAwareTrainStep inside the Task.Run loop (passing the preprocessed inputs
from PreprocessTokens/PreprocessLabels, the optimizer, Layers, and reporting
progress), remove the unused
LossFunction.CalculateLoss/LossFunction.CalculateDerivative and the dummy gt
tensor, and keep SetTrainingMode(true/false) and _optimizer.UpdateParameters
usage consistent with RunCrfAwareTrainStep integration.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: b53e8c52-ab7a-44e4-9da1-a2e996550c1b
📒 Files selected for processing (6)
src/Helpers/LayerHelper.cssrc/NER/SequenceLabeling/BiLSTMCRF.cssrc/NER/SequenceLabeling/CNNBiLSTMCRF.cssrc/NER/SequenceLabeling/LSTMCRF.cssrc/NeuralNetworks/Layers/ConditionalRandomFieldLayer.cssrc/VisionLanguage/Document/Donut.cs
Completes the VisionLanguage Donut fix so its full model-family suite passes (25/25), and cleans up the file's formatting. - Donut.Train overshot on the first AdamW step because the optimizer used the AdamW default lr=1e-3. Donut (Kim et al. 2022 §4) fine-tunes at ~1e-4, so set the paper-faithful learning rate on the native-mode optimizer. - The paper-scale defaults (VisionDim=1024, 16 layers) make one train step ~9s on CPU, and the memorization invariant needs dropout disabled for a clean monotonic decrease — neither fits the auto-generated paper-scale path. Follow the established JanusPro pattern: exclude Donut from auto-generation and add a manual reduced-scale DonutTests scaffold (same architecture shape, ~4x smaller dims, DropoutRate=0) that exercises every code path in seconds. - Reformatted VisionLanguage/Document/Donut.cs from dense single-line method bodies to standard multi-line C# (no behavioural change). Verified: DonutTests 25/25 pass (Clone, Clone_AfterTraining, Training_ShouldReduceLoss, LossStrictlyDecreasesOnMemorizationTask, MoreData_ShouldNotDegrade, etc.). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/VisionLanguage/Document/Donut.cs (1)
285-301:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRecompute encoder/decoder boundary after native deserialization (blocking correctness risk).
_encoderLayerEndis consumed in Line 124 and Line 164, but after reading deserialized options you never rebind it in native mode. A stale boundary can route layers to the wrong stage.Suggested fix
protected override void DeserializeNetworkSpecificData(BinaryReader reader) { _useNativeMode = reader.ReadBoolean(); string mp = reader.ReadString(); if (!string.IsNullOrEmpty(mp)) _options.ModelPath = mp; @@ _options.MaxOutputTokens = reader.ReadInt32(); _options.EncoderType = reader.ReadString(); - if (!_useNativeMode && _options.ModelPath is { } p && !string.IsNullOrEmpty(p)) + if (_useNativeMode) + { + // Rebind cached encoder/decoder split to deserialized config. + ComputeEncoderDecoderBoundary(); + if (_encoderLayerEnd < 0 || _encoderLayerEnd > Layers.Count) + _encoderLayerEnd = Math.Clamp(_encoderLayerEnd, 0, Layers.Count); + } + else if (_options.ModelPath is { } p && !string.IsNullOrEmpty(p)) OnnxModel = new OnnxModel<T>(p, _options.OnnxOptions); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/VisionLanguage/Document/Donut.cs` around lines 285 - 301, After deserializing network-specific fields in DeserializeNetworkSpecificData, recompute and rebind the encoder/decoder boundary (_encoderLayerEnd) when running in native mode (_useNativeMode) so the layer routing is correct; specifically, after reading _options (NumVisionLayers, NumDecoderLayers, NumHeads, EncoderType, etc.) call the existing routine that computes/sets the encoder boundary (e.g., RecomputeEncoderLayerEnd() / ComputeEncoderDecoderBoundary() / the same logic used where _encoderLayerEnd is normally initialized) so that subsequent consumers of _encoderLayerEnd (lines that reference it) see the updated boundary for the deserialized configuration.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/VisionLanguage/Document/Donut.cs`:
- Around line 303-308: CreateNewInstance currently passes the shared _options
reference into new Donut<T> instances which allows cross-instance mutation; fix
by passing a cloned/copy of DonutOptions instead of the original _options (e.g.,
construct a new DonutOptions from _options or call a Clone/Copy method) when
calling the Donut<T> constructors in CreateNewInstance so each Donut gets its
own immutable/config-isolated options object.
---
Outside diff comments:
In `@src/VisionLanguage/Document/Donut.cs`:
- Around line 285-301: After deserializing network-specific fields in
DeserializeNetworkSpecificData, recompute and rebind the encoder/decoder
boundary (_encoderLayerEnd) when running in native mode (_useNativeMode) so the
layer routing is correct; specifically, after reading _options (NumVisionLayers,
NumDecoderLayers, NumHeads, EncoderType, etc.) call the existing routine that
computes/sets the encoder boundary (e.g., RecomputeEncoderLayerEnd() /
ComputeEncoderDecoderBoundary() / the same logic used where _encoderLayerEnd is
normally initialized) so that subsequent consumers of _encoderLayerEnd (lines
that reference it) see the updated boundary for the deserialized configuration.
🪄 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: 2ce020a5-8fec-4afe-9258-5b3cc2c1537c
📒 Files selected for processing (3)
src/AiDotNet.Generators/TestScaffoldGenerator.cssrc/VisionLanguage/Document/Donut.cstests/AiDotNet.Tests/ModelFamilyTests/NeuralNetworks/DonutTests.cs
…odels
Replace the AdamW optimizer on LSTMCRF / BiLSTMCRF / CNNBiLSTMCRF with the exact
training recipe from their papers:
- Lample et al. 2016 ("Neural Architectures for NER", §4): SGD lr=0.01,
learning-rate decay 0.05, gradient clipping 5.0.
- Ma & Hovy 2016 ("End-to-end Sequence Labeling via Bi-LSTM-CNNs-CRF", §3.3):
SGD lr≈0.015, decay 0.05, gradient clipping 5.0.
AdamW's adaptive per-parameter steps oscillate and diverge on long synthetic
runs (MoreData_ShouldNotDegrade: 200-iter loss climbed above the 50-iter loss).
Clipped SGD with the lr_t = lr_0 / (1 + 0.05*t) decay (LambdaLRScheduler) is the
paper recipe and trains stably; MoreData now passes, alongside clone,
clone-after-training, gradient-flow, loss-decrease and memorization.
Default LearningRate updated to the paper SGD value (0.01 / 0.015) and
gradient clipping configured at MaxGradientNorm=5.0 (ByNorm).
Known residual: DifferentInputs_AfterTraining can still report identical output
for distinct inputs — the CRF's discrete Viterbi decode picks the same label
path once the transition scores dominate after training (the continuous
emissions still respond to input). This is a discrete-output test artifact
(it was flaky under AdamW too), not broken gradient flow, and is inherent to a
paper-faithful trained CRF producing confident sequence predictions.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/NER/SequenceLabeling/LSTMCRF.cs (1)
225-250:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winBLOCKING:
TrainAsyncis inconsistent withTrainand other NER models, contains dead code.This is a blocking issue that must be fixed before merge:
Inconsistent training path:
Train()(line 347) correctly delegates toRunCrfAwareTrainStep, butTrainAsyncstill uses the old manual training loop. This means synchronous and asynchronous training use different objectives—async won't use CRF NLL whenUseCRF=true.Dead code: Lines 233-235 compute
gradandgtbutgtis never used. The comment "Backward removed — tape-based training handles gradients" suggests incomplete refactoring.Pattern violation: Both
BiLSTMCRF.TrainAsyncandCNNBiLSTMCRF.TrainAsyncuseRunCrfAwareTrainStep. This model should follow the same pattern.🐛 Proposed fix: align TrainAsync with Train and sibling models
return Task.Run(() => { for (int epoch = 1; epoch <= epochs; epoch++) { cancellationToken.ThrowIfCancellationRequested(); - SetTrainingMode(true); - try - { - var preprocessed = PreprocessTokens(tokenEmbeddings); - var preprocessedLabels = PreprocessLabels(labels, preprocessed.Shape[0]); - var output = Forward(preprocessed); - double loss = NumOps.ToDouble(LossFunction.CalculateLoss( - output.ToVector(), preprocessedLabels.ToVector())); - var grad = LossFunction.CalculateDerivative(output.ToVector(), preprocessedLabels.ToVector()); - var gt = Tensor<T>.FromVector(grad); - // Backward removed — tape-based training handles gradients - _optimizer.UpdateParameters(Layers); - - progress?.Report(new NERTrainingProgress - { - CurrentEpoch = epoch, - TotalEpochs = epochs, - CurrentBatch = 1, - TotalBatches = 1, - Loss = loss - }); - } - finally - { - SetTrainingMode(false); - } + // Shared CRF-aware step — routes through CRF NLL when UseCRF=true + // so the async path doesn't silently train on a non-differentiable + // Viterbi-argmax objective. + double loss = RunCrfAwareTrainStep( + tokenEmbeddings, labels, _options.UseCRF, _optimizer); + + progress?.Report(new NERTrainingProgress + { + CurrentEpoch = epoch, + TotalEpochs = epochs, + CurrentBatch = 1, + TotalBatches = 1, + Loss = loss + }); } }, cancellationToken);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/NER/SequenceLabeling/LSTMCRF.cs` around lines 225 - 250, TrainAsync currently contains a leftover manual training loop and unused gradient variables (grad, gt) and therefore diverges from Train and sibling models; replace the manual loop in TrainAsync with a call to RunCrfAwareTrainStep (the same pattern used by Train and by BiLSTMCRF/CNNBiLSTMCRF.TrainAsync), preserve SetTrainingMode(true/false) and progress reporting, ensure RunCrfAwareTrainStep is invoked with the same parameters so UseCRF is respected (CRF NLL is used when UseCRF==true), and remove the dead computations of grad and gt plus the comment about backward handling and the direct _optimizer.UpdateParameters(Layers) call so gradients are handled consistently by the shared train step.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/NER/SequenceLabeling/LSTMCRF.cs`:
- Around line 225-250: TrainAsync currently contains a leftover manual training
loop and unused gradient variables (grad, gt) and therefore diverges from Train
and sibling models; replace the manual loop in TrainAsync with a call to
RunCrfAwareTrainStep (the same pattern used by Train and by
BiLSTMCRF/CNNBiLSTMCRF.TrainAsync), preserve SetTrainingMode(true/false) and
progress reporting, ensure RunCrfAwareTrainStep is invoked with the same
parameters so UseCRF is respected (CRF NLL is used when UseCRF==true), and
remove the dead computations of grad and gt plus the comment about backward
handling and the direct _optimizer.UpdateParameters(Layers) call so gradients
are handled consistently by the shared train step.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 21aa63c4-e464-48ff-8ad7-bf29c7d88721
📒 Files selected for processing (6)
src/NER/Options/BiLSTMCRFOptions.cssrc/NER/Options/CNNBiLSTMCRFOptions.cssrc/NER/Options/LSTMCRFOptions.cssrc/NER/SequenceLabeling/BiLSTMCRF.cssrc/NER/SequenceLabeling/CNNBiLSTMCRF.cssrc/NER/SequenceLabeling/LSTMCRF.cs
- Dispose the existing OnnxModel before replacing it in DeserializeNetworkSpecificData across the ONNX-mode video models (Upscale4KAgent, VideoGigaGAN, ABME, AMT, BiMVFI, GIMMVFI, IFRNet) and VisionLanguage Donut. Reassigning without disposing leaked the native ONNX session on every deserialize / clone round-trip. - Donut.CreateNewInstance now copies its options (new DonutOptions(_options)) instead of sharing the reference, so a cloned/new instance can't mutate the source's options via its deserialize path (writes EncoderType / ModelPath back into options). - Donut (PixelToSequence) serialize uses NumOps.ToDouble instead of Convert.ToDouble for consistency with the library's generic numeric conversions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Started as #1465 (34 models re-randomizing weights on deserialize) and grew to fix the layer- and model-level bugs that work surfaced — all fixes are paper-faithful.
1. Deserialize weight preservation (#1465)
34 Video / NER / OCR / TTS models called
Layers.Clear(); InitializeLayers()insideDeserializeNetworkSpecificData. The baseDeserializeInternalUncheckedalready recreates every layer with its saved weights first, so re-initializing discarded them and left the model random — breaking clone (DeepCopy) and load-from-disk. Fix: drop the clear+re-init (keep the ONNX-session rebuild); for ref-caching models (OpenSora) re-point cached references at the deserializedLayers.SlowFastwas a false positive (already correct) and left untouched.2. Layer-serialization round-trip (unblocked Donut clone)
SwinTransformerBlockLayer/SwinPatchEmbeddingLayernow emit their constructor settings (NumHeads/WindowSize/ShiftSize/MlpRatio,PatchSize/EmbedDim) viaGetMetadata, so deserialization reconstructs them with the correct dims (previously threw / produced wrong ParameterCount).Layersand serializes/restores its network-level token/position embeddings. Predict→Clone→Predict is now bit-identical.3. Paper-faithful CRF / Donut / NER
ConditionalRandomFieldLayeris now variable-length (Lample et al. 2016 — the transition matrix + start/end scores are length-independent); removed the fixed-sequenceLengththrow on CPU and GPU paths.PreprocessTokenstruncate-only (no force-pad to MaxSequenceLength); train the CRF NLL (Lample 2016) via the shared CRF-aware step; switched to the paper optimizer: SGD lr=0.01 (0.015 for Ma & Hovy 2016) + lr-decay 0.05 + gradient clipping 5.0.Validation
Known residual (not a regression)
DifferentInputs_AfterTrainingfor the CRF NER models can report identical output for distinct inputs: after paper-faithful training the CRF's discrete Viterbi decode picks the same label path once transition scores dominate (the continuous emissions still respond to input). This is a discrete-output test artifact — flaky under the previous AdamW too — not broken gradient flow.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Improvements
Tests