Repository navigation
fix: fix #700: diffusion conv GPU training + auto eigenbasis - #716
Conversation
This commit consolidates changes from a PR where commit messages did not follow conventional commits format. The code changes are preserved exactly as originally authored. Note: Original commit history was consolidated due to message format issues. Review the PR for the full change context. Co-Authored-By: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Commit Messages Auto-FixedThe commitlint check failed because one or more commit messages did not follow Conventional Commits format. Action taken - All non-compliant commits have been fixed to follow the conventional commits format. Changes made:
The PR branch has been force-pushed with the fixed commits. If you had local changes, you may need to git pull --rebase. |
|
🤖 PR Title Auto-Fixed Your PR title was automatically updated to follow Conventional Commits format. Original title: New title: Detected type: Valid types and their effects:
If the detected type is incorrect, you can manually edit the PR title. |
f15f0e4 to
701603e
Compare
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📒 Files selected for processing (2)
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. WalkthroughThis PR adds GPU training support to ConvLSTMLayer and DiffusionConvLayer by introducing SupportsGpuTraining properties, implementing GPU-based backward pass and parameter updates, adding tensor caching mechanisms, and auto-generating eigenbasis from Laplacian matrices as needed. Documentation is updated to reflect these GPU implementations. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Layer as DiffusionConvLayer
participant GPU as GPU Backend
participant Cache as Tensor Cache
participant Optim as Optimizer
User->>Layer: ForwardGpu(input, training=true)
Layer->>Layer: EnsureEigenbasisForExecution()
Layer->>GPU: Allocate GPU tensors
GPU->>Cache: Store input, diffused_features, output
Layer-->>User: output
User->>Layer: BackwardGpu(loss_gradient)
Layer->>Cache: Retrieve cached forward tensors
Cache-->>Layer: input, diffused_features, output
Layer->>GPU: Compute weight/bias/diffusion gradients
GPU->>GPU: Spectral or direct diffusion path
GPU->>Cache: Store gradients
Layer->>GPU: Compute input gradients
Layer-->>User: input_gradient
User->>Layer: UpdateParametersGpu(optimizer)
Layer->>Optim: Get optimizer state (velocity, moments)
Layer->>GPU: Apply updates with optimizer
Layer->>Layer: Clamp diffusion times
Layer->>Cache: Clear GPU cache post-update
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR enables GPU training for DiffusionConvLayer and marks ConvLSTMLayer as GPU-training-capable by implementing GPU-resident backward passes and parameter updates.
Changes:
- Implemented full GPU backward pass for
DiffusionConvLayerwith cached tensors and gradient computation - Added automatic eigenbasis computation from Laplacian for both GPU and CPU execution paths
- Updated documentation to reflect GPU training support for both layers
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| src/NeuralNetworks/Layers/DiffusionConvLayer.cs | Implements GPU backward pass, automatic eigenbasis computation, and GPU parameter updates |
| src/NeuralNetworks/Layers/ConvLSTMLayer.cs | Updates comments to reflect existing GPU training support |
| docs/GPU_TRAINING_STATUS.md | Marks ConvLSTMLayer as fully GPU-capable |
| docs/GPU_KERNEL_STATUS.md | Updates kernel status for ConvLSTM and DiffusionConv layers |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/NeuralNetworks/Layers/DiffusionConvLayer.cs (1)
788-1060: ForwardGpu performs repeated GPU→CPU→GPU transfers per time scale, severely limiting performance.The inner loop (lines 984-1005) downloads both
timeOutputBufferand the entirediffusedBuffer, scatters on CPU, then re-uploads—repeated for each time scale. This causes O(NumTimeScales) roundtrips and defeats GPU-resident training efficiency.Additionally, two buffers are allocated but unused:
decayBuffer(line 901) is never referenceddecayTiledBuffer(line 929) is never used (code usesdecayTiledUploadBufferinstead)Replace the CPU-side scatter with a device-side strided copy operation and remove the unused allocations.
🤖 Fix all issues with AI agents
In `@docs/GPU_TRAINING_STATUS.md`:
- Around line 324-325: Add a short explanatory note near the
ConvLSTMLayer/GRULayer table clarifying that GPU training support depends on
using the DirectGpu backend and on having ForwardGpu run in training mode to
cache activations (since ConvLSTMLayer uses composed kernels + cached forward
state) before invoking BackwardGpu; mention these prerequisites explicitly and
link them to the symbols DirectGpu, ForwardGpu, BackwardGpu and ConvLSTMLayer so
readers know the required execution order and backend for GPU training to work.
In `@src/NeuralNetworks/Layers/DiffusionConvLayer.cs`:
- Around line 462-472: Compute the eigenbasis before entering any Parallel.For
by calling EnsureEigenbasisForExecution (or extracting its eigen-computation
logic) from ProcessBatched and only after SetLaplacian/_massMatrix changes, or
else guard the mutation of _eigenvalues/_eigenvectors in
EnsureEigenbasisForExecution with a thread-safe lock/double-checked pattern to
avoid races from ComputeDiffusedFeatures; additionally, if a non-null
_massMatrix is meaningful for the Laplacian, replace the plain L
eigen-decomposition with a generalized eigenproblem solver for L φ = λ M φ (or
explicitly document that only standard L decomposition is supported), updating
EnsureEigenbasisForExecution and any helpers used by ComputeDiffusedFeatures to
use the generalized solution when _massMatrix != null.
🧹 Nitpick comments (3)
src/NeuralNetworks/Layers/DiffusionConvLayer.cs (3)
2-4: New API/behavior knobs look reasonable; please document semantics and persistence.
preferSpectralDiffusion: bool?is subtle (null/true/false). Consider an enum for readability, or at least add a brief note in class-level docs + XML param docs about when eigenbasis computation happens and its cost.SupportsGpuTraining => trueis fine, but the runtime requirement is effectively “GPU training supported when eigenbasis is available or Laplacian is provided and we can compute eigenbasis.”Also applies to: 98-102, 178-187
1064-1399: BackwardGpu structure is solid, but it’s very allocation-heavy; consider caching & avoiding CPU materializations.
- Many per-time-scale allocations (
weightsSlice,decayTiledData,derivTiledData) and repeated GPU uploads will add overhead; consider keeping weights/eigenbasis and per-t buffers on GPU once (especially since the PR goal is GPU-resident training).- Fallback path downloads to CPU for activation derivative (Line 1165-1172). That’s acceptable as a fallback, but it’d be good to explicitly document that custom/non-fused activations will incur host transfers.
2010-2016: Clone/ResetState updates are consistent; consider whether preferSpectralDiffusion should serialize.
- Good:
Clone()propagates_preferSpectralDiffusion(Line 2010-2016) andResetState()clears GPU caches (Line 2040-2059).- If
_preferSpectralDiffusionmaterially changes runtime behavior, consider serializing it too (otherwise deserialization will silently revert to the constructor default).Also applies to: 2040-2059
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
docs/GPU_KERNEL_STATUS.mddocs/GPU_TRAINING_STATUS.mdsrc/NeuralNetworks/Layers/ConvLSTMLayer.cssrc/NeuralNetworks/Layers/DiffusionConvLayer.cs
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-12-18T08:49:25.295Z
Learnt from: ooples
Repo: ooples/AiDotNet PR: 444
File: src/Interfaces/IPruningMask.cs:1-102
Timestamp: 2025-12-18T08:49:25.295Z
Learning: In the AiDotNet repository, the project-level global using includes AiDotNet.Tensors.LinearAlgebra via AiDotNet.csproj. Therefore, Vector<T>, Matrix<T>, and Tensor<T> are available without per-file using directives. Do not flag missing using directives for these types in any C# files within this project. Apply this guideline broadly to all C# files (not just a single file) to avoid false positives. If a file uses a type from a different namespace not covered by the global using, flag as usual.
Applied to files:
src/NeuralNetworks/Layers/ConvLSTMLayer.cssrc/NeuralNetworks/Layers/DiffusionConvLayer.cs
📚 Learning: 2025-12-18T08:49:53.103Z
Learnt from: ooples
Repo: ooples/AiDotNet PR: 444
File: src/Interfaces/IPruningStrategy.cs:1-4
Timestamp: 2025-12-18T08:49:53.103Z
Learning: In this repository, global using directives are declared in AiDotNet.csproj for core namespaces (AiDotNet.Tensors.* and AiDotNet.*) and common system types. When reviewing C# files, assume these global usings are in effect; avoid adding duplicate using statements for these namespaces and for types like Vector<T>, Matrix<T>, Tensor<T>, etc. If a type is not found, verify the global usings or consider adding a file-scoped using if needed. Prefer relying on global usings to reduce boilerplate.
Applied to files:
src/NeuralNetworks/Layers/ConvLSTMLayer.cssrc/NeuralNetworks/Layers/DiffusionConvLayer.cs
📚 Learning: 2025-12-19T19:05:13.598Z
Learnt from: ooples
Repo: ooples/AiDotNet PR: 445
File: src/Interfaces/IPredictionModelBuilder.cs:7-8
Timestamp: 2025-12-19T19:05:13.598Z
Learning: In ooples/AiDotNet, IAggregationStrategy<TModel> is declared in AiDotNet.Interfaces (src/Interfaces/IAggregationStrategy.cs). For src/Interfaces/IPredictionModelBuilder.cs (also in AiDotNet.Interfaces), referencing IAggregationStrategy does not require an additional using. Avoid flagging missing using directives for this case in future reviews.
Applied to files:
src/NeuralNetworks/Layers/DiffusionConvLayer.cs
🔇 Additional comments (4)
docs/GPU_KERNEL_STATUS.md (1)
183-184: Good status change; ensure⚠️ meaning is consistently “no dedicated kernel, composed ops.”The
⚠️ entries read correctly as “composed from Conv2D/GEMM/elementwise ops” (not a standalone kernel). Consider adding the same “composed” qualifier anywhere these appear in the doc (including the priority list) to prevent readers assuming partial backend parity issues vs composition.Also applies to: 231-232, 286-287
src/NeuralNetworks/Layers/ConvLSTMLayer.cs (2)
246-250: SupportsGpuTraining=>true is a behavior contract: please gate it behind validated BackwardGpu/UpdateParametersGpu.With this flip, training will route through GPU BPTT and GPU updates; any subtle shape/lifetime bugs in those paths become release-blocking. I’d strongly recommend adding at least one integration test that runs ForwardGpu(train) → BackwardGpu → UpdateParametersGpu and asserts loss decreases / gradients finite.
649-650: ForwardGpu doc update matches the new training path.Nice to explicitly document the cache dependency for GPU BPTT; that helps prevent misuse (ForwardGpu inference-only → BackwardGpu).
src/NeuralNetworks/Layers/DiffusionConvLayer.cs (1)
1405-1455: UpdateParametersGpu: diffusion-times update/clamp is good; verify optimizer-state mapping matches config expectations.You reuse
Velocitybuffers asSquaredAvg/AccumulatedGrad. IfIGpuOptimizerConfig.ApplyUpdateinterprets these fields differently per optimizer, double-check that RMSProp/Adagrad aren't accidentally sharing a buffer with momentum/velocity semantics.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
|
|
||
| lock (_eigenbasisLock) | ||
| { | ||
| if (_eigenvalues != null && _eigenvectors != null) |
Check warning
Code scanning / CodeQL
Constant condition Warning
Copilot Autofix
AI 9 months ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
| if (_eigenvalues == null || _eigenvectors == null) | ||
| { | ||
| if (_preferSpectralDiffusion != false) | ||
| { | ||
| EnsureEigenbasisForExecution(); | ||
| } | ||
| } |
Check notice
Code scanning / CodeQL
Nested 'if' statements can be combined Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
In general, to fix this pattern you merge the outer and inner if conditions into a single if statement using &&, since both must be true to execute the body. This removes unnecessary nesting and improves readability while preserving semantics.
Concretely, in ComputeDiffusedFeatures in src/NeuralNetworks/Layers/DiffusionConvLayer.cs, replace the nested structure:
if (_eigenvalues == null || _eigenvectors == null)
{
if (_preferSpectralDiffusion != false)
{
EnsureEigenbasisForExecution();
}
}with a single combined condition:
if ((_eigenvalues == null || _eigenvectors == null) && _preferSpectralDiffusion != false)
{
EnsureEigenbasisForExecution();
}This preserves operator precedence clearly via parentheses, maintains identical short‑circuiting (the second part is only evaluated if the first is true), and does not change any subsequent logic. No new methods, imports, or auxiliary definitions are required.
| @@ -603,12 +603,9 @@ | ||
| int diffusedSize = InputChannels * NumTimeScales; | ||
| var diffused = new T[numVertices * diffusedSize]; | ||
|
|
||
| if (_eigenvalues == null || _eigenvectors == null) | ||
| if ((_eigenvalues == null || _eigenvectors == null) && _preferSpectralDiffusion != false) | ||
| { | ||
| if (_preferSpectralDiffusion != false) | ||
| { | ||
| EnsureEigenbasisForExecution(); | ||
| } | ||
| EnsureEigenbasisForExecution(); | ||
| } | ||
|
|
||
| if (_eigenvalues != null && _eigenvectors != null) |
|
|
||
| // Allocate diffused output buffer [batchSize * numVertices, diffusedSize] | ||
| using var diffusedBuffer = backend.AllocateBuffer(batchSize * numVertices * diffusedSize); | ||
| var diffusedBuffer = backend.AllocateBuffer(batchSize * numVertices * diffusedSize); |
Check notice
Code scanning / CodeQL
Missed 'using' opportunity Note
Copilot Autofix
AI 9 months ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
| var diffusedBuffer = backend.AllocateBuffer(batchSize * numVertices * diffusedSize); | ||
| backend.Fill(diffusedBuffer, 0.0f, batchSize * numVertices * diffusedSize); | ||
| var diffusedRetained = false; | ||
| IGpuBuffer? preActivationBuffer = null; |
Check notice
Code scanning / CodeQL
Missed 'using' opportunity Note
Copilot Autofix
AI 9 months ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
| ? [numVertices, OutputChannels] | ||
| : [batchSize, numVertices, OutputChannels]; | ||
| for (int t = 0; t < NumTimeScales; t++) | ||
| { |
Check notice
Code scanning / CodeQL
Missed 'using' opportunity Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
In general, the fix is to replace the manual “declare resource, assign in try, dispose in finally” pattern with C# using resource management. That can be done either as using statements with blocks or as using declarations, which dispose the resource at the end of the containing scope.
In this method, the cleanest change without altering functionality is:
- Keep the existing
try/catch/finallystructure for any non‑disposal logic. - Convert
weightGradBuffer,biasGradBuffer, andinputGradBuffertousingdeclarations so they are automatically disposed at the end of thetryblock’s scope. - Remove the explicit
finallyblock that callsDispose()on each buffer, as it becomes redundant.
Concretely:
- Around lines 1166–1168, change the three nullable variables to
using IGpuBuffer? ... = null;. - Around lines 1437–1442, remove the
finallyblock that disposes them, leaving the closing brace of the method directly after thereturn inputGradTensor;.
No new imports, methods, or definitions are needed; this relies only on standard C# using semantics.
| @@ -1163,9 +1163,9 @@ | ||
| if (outputGrad2D.Shape[0] != outputRows || outputGrad2D.Shape[1] != outputCols) | ||
| throw new ArgumentException("Output gradient shape does not match cached output."); | ||
|
|
||
| IGpuBuffer? weightGradBuffer = null; | ||
| IGpuBuffer? biasGradBuffer = null; | ||
| IGpuBuffer? inputGradBuffer = null; | ||
| using IGpuBuffer? weightGradBuffer = null; | ||
| using IGpuBuffer? biasGradBuffer = null; | ||
| using IGpuBuffer? inputGradBuffer = null; | ||
|
|
||
| try | ||
| { | ||
| @@ -1434,12 +1434,6 @@ | ||
| inputGradBuffer = null; | ||
| return inputGradTensor; | ||
| } | ||
| finally | ||
| { | ||
| weightGradBuffer?.Dispose(); | ||
| biasGradBuffer?.Dispose(); | ||
| inputGradBuffer?.Dispose(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> |
| : [batchSize, numVertices, OutputChannels]; | ||
| for (int t = 0; t < NumTimeScales; t++) | ||
| { | ||
| float time = (float)NumOps.ToDouble(DiffusionTimes[t]); |
Check notice
Code scanning / CodeQL
Missed 'using' opportunity Note
| for (int t = 0; t < NumTimeScales; t++) | ||
| { | ||
| float time = (float)NumOps.ToDouble(DiffusionTimes[t]); | ||
| var decayData = new float[numEig]; |
Check notice
Code scanning / CodeQL
Missed 'using' opportunity Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
In general, to fix this kind of issue you replace a try/finally pattern that manually calls Dispose with a using statement that automatically disposes the resource when control leaves the scope, including when exceptions are thrown. When ownership of the resource may be transferred (so that it should not be disposed at the end of the method), you introduce a separate local variable managed by using and clear it when ownership is transferred. The disposable field or outer variable itself does not need to be in a using; only the temporary handle does.
For this specific method, weightGradBuffer, biasGradBuffer, and inputGradBuffer are declared before the try and disposed in the finally. CodeQL flags inputGradBuffer, but the same reasoning applies to all three. The best fix without changing functionality is to:
- Introduce separate “owned” local variables that are actually created in the
tryblock (for example,weightGradOwned,biasGradOwned,inputGradOwned) and wrap each of those in its ownusingstatement. - Keep
weightGradBuffer,biasGradBuffer, andinputGradBufferas the variables that are passed into the created tensors, but have them just reference the owned buffers. - When ownership is transferred to a
GpuTensor<T>(viaownsBuffer: true), set the corresponding owned variable tonull(or otherwise clear it) so that theusingdoes not attempt to dispose it again. This emulates the existing “set to null before finally” behavior. - Remove the
finallyblock for these three variables, since disposal is now handled by theusingstatements, while preserving all other logic.
Concretely, inside BackwardGpu, in the region where weightGradBuffer, biasGradBuffer, and inputGradBuffer are used, we’ll:
- Replace the single big
try/finallythat disposes them with nestedusingblocks for newly introduced owned locals. - Update buffer allocations to assign to the owned locals, then to the original buffer variables.
- Before constructing
GpuTensor<T>instances withownsBuffer: true, set the owned local tonulland keep returning as before. - Remove the explicit
.Dispose()calls in thefinallyblock.
This change is fully contained within BackwardGpu in src/NeuralNetworks/Layers/DiffusionConvLayer.cs and requires no new imports or additional methods.
| @@ -1167,10 +1167,17 @@ | ||
| IGpuBuffer? biasGradBuffer = null; | ||
| IGpuBuffer? inputGradBuffer = null; | ||
|
|
||
| try | ||
| IGpuBuffer? weightGradOwned = null; | ||
| IGpuBuffer? biasGradOwned = null; | ||
| IGpuBuffer? inputGradOwned = null; | ||
|
|
||
| int outputGradSize = outputRows * outputCols; | ||
| using var deltaBuffer = backend.AllocateBuffer(outputGradSize); | ||
| using (weightGradOwned = null) | ||
| using (biasGradOwned = null) | ||
| using (inputGradOwned = null) | ||
| { | ||
| int outputGradSize = outputRows * outputCols; | ||
| using var deltaBuffer = backend.AllocateBuffer(outputGradSize); | ||
| int _ = outputGradSize; // keep at least one statement before existing logic | ||
|
|
||
| bool activationHandled = ApplyActivationBackwardGpu( | ||
| backend, | ||
| @@ -1431,15 +1437,10 @@ | ||
| inputGradShape, | ||
| GpuTensorRole.Gradient, | ||
| ownsBuffer: true); | ||
| inputGradOwned = null; | ||
| inputGradBuffer = null; | ||
| return inputGradTensor; | ||
| } | ||
| finally | ||
| { | ||
| weightGradBuffer?.Dispose(); | ||
| biasGradBuffer?.Dispose(); | ||
| inputGradBuffer?.Dispose(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> |
| { | ||
| var batchedEigBuffer = tiledEigBuffer | ||
| ?? throw new InvalidOperationException("Batched eigenvector buffers were not initialized."); | ||
| backend.BatchedGemm(batchedEigBuffer, spectralGradBuffer, spatialGradBuffer, |
Check notice
Code scanning / CodeQL
Missed 'using' opportunity Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
In general, the fix is to replace the manual try/finally-based disposal of IGpuBuffer instances with using statements (or using var declarations), so that the C# compiler automatically generates the correct try/finally disposal logic. This makes the intent clearer and reduces boilerplate.
Concretely here, the best fix is:
- Change
tiledEigBufferandtiledEigTBufferfrom nullable fields initialized tonulland disposed in afinallyblock, tousing varlocal variables that are conditionally assigned whenbatchSize > 1. - Remove the explicit
try/finallyblock and the manual.Dispose()calls. - Keep the rest of the logic (conditional allocation, usage, and the null-coalescing/exception check before
BatchedGemm) unchanged, so functional behavior remains the same. - No additional imports or helper methods are required;
using varworks directly onIGpuBuffer.
The scope of these using var declarations will be the current method’s body (or the surrounding block where we declare them), which is effectively the same as the former try scope plus finally, so resources will still be disposed after all their uses, including when exceptions are thrown.
| var batchedEigBuffer = tiledEigBuffer | ||
| ?? throw new InvalidOperationException("Batched eigenvector buffers were not initialized."); | ||
| backend.BatchedGemm(batchedEigBuffer, spectralGradBuffer, spatialGradBuffer, | ||
| numVertices, inputChannels, numEig, batchSize); |
Check notice
Code scanning / CodeQL
Missed 'using' opportunity Note
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 9 months ago
General fix: Replace the manual try/finally with explicit using-based lifetime management for the disposable GPU buffers. The goal is to keep semantics identical: only allocate the batched buffers when batchSize > 1, use them where needed, and ensure they are disposed afterwards, even on exceptions.
Best concrete fix in this snippet:
- Keep
tiledEigBufferandtiledEigTBufferas locals inside thebatchSize > 1branch, but wrap them inusingblocks so they are always disposed when leaving that branch. - For
batchSize == 1, we don’t allocate these buffers at all, so we don’t need nullable fields or afinally. - Because the code after the
ifneeds access totiledEigTBuffer(in theelsebranch at line 1250), we can:- Move the
if (batchSize > 1)block up into its ownif/elsearound the GEMM call so that theusing-scoped buffer is used entirely inside that block; or - Use C# using declarations for
tiledEigBufferandtiledEigTBufferthat live from their declaration to the end of the containing scope.
- Move the
- The smallest change within the shown region is to turn the existing
try/finallyinto ausing-declaration style: declareusing IGpuBuffer? tiledEigBuffer = null; using IGpuBuffer? tiledEigTBuffer = null;and remove thefinallydisposal, sinceusingdeclarations will automatically callDisposeat scope end. However,usingdeclarations cannot be nullable where you want to skip allocation; they can, butDisposewill be called onnulland must be safe. To avoid relying on that, a better adjustment is:- Keep them nullable but declare them with
usingdeclarations and rely on the runtime not callingDisposeonnull. In C#, the compiler emits a null check beforeDispose, so this is safe.
- Keep them nullable but declare them with
- Thus:
- Change
IGpuBuffer? tiledEigBuffer = null;tousing IGpuBuffer? tiledEigBuffer = null; - Change
IGpuBuffer? tiledEigTBuffer = null;tousing IGpuBuffer? tiledEigTBuffer = null; - Remove the
try/finallywrapper and its manualDisposecalls, just leaving the body of the oldtryblock in place.
- Change
- No new imports or additional methods are required.
Line‑level changes:
- In
DiffusionConvLayer<T>method containing lines 1230–1382 insrc/NeuralNetworks/Layers/DiffusionConvLayer.cs:- Replace the declarations of
tiledEigBufferandtiledEigTBufferwithusingdeclarations. - Remove the
try/finallystructure, keeping only the formertryblock contents in place.
- Replace the declarations of
| numVertices, inputChannels, numEig, batchSize); | ||
| } | ||
|
|
||
| using var productBuffer = backend.AllocateBuffer(outputRows * inputChannels); | ||
| backend.Multiply(spatialDerivBuffer, diffusedGradBuffer, productBuffer, | ||
| outputRows * inputChannels); | ||
|
|
||
| float timeGrad = backend.Sum(productBuffer, outputRows * inputChannels); | ||
| _diffusionTimesGradient[t] = NumOps.FromDouble(timeGrad); | ||
| } | ||
| } | ||
| finally | ||
| { | ||
| tiledEigBuffer?.Dispose(); | ||
| tiledEigTBuffer?.Dispose(); | ||
| } | ||
|
|
||
| var weightGradData = backend.DownloadBuffer(weightGradBuffer); | ||
| _weightsGradient = new Tensor<T>( | ||
| DirectGpuEngine.FromFloatArray<T>(weightGradData), | ||
| _weights.Shape); | ||
|
|
||
| var biasGradData = backend.DownloadBuffer(biasGradBuffer); | ||
| _biasesGradient = new Tensor<T>( | ||
| DirectGpuEngine.FromFloatArray<T>(biasGradData), | ||
| _biases.Shape); | ||
|
|
||
| _gpuWeightsGradient?.Dispose(); | ||
| _gpuWeightsGradient = new GpuTensor<T>( | ||
| backend, | ||
| weightGradBuffer, | ||
| _weights.Shape, | ||
| GpuTensorRole.Gradient, | ||
| ownsBuffer: true); | ||
| weightGradBuffer = null; | ||
|
|
||
| _gpuBiasesGradient?.Dispose(); | ||
| _gpuBiasesGradient = new GpuTensor<T>( | ||
| backend, | ||
| biasGradBuffer, | ||
| _biases.Shape, | ||
| GpuTensorRole.Gradient, | ||
| ownsBuffer: true); | ||
| biasGradBuffer = null; | ||
|
|
||
| if (_diffusionTimesGradient != null) | ||
| { | ||
| _gpuDiffusionTimesGradient?.Dispose(); | ||
| _gpuDiffusionTimesGradient = new GpuTensor<T>( | ||
| backend, | ||
| _diffusionTimesGradient, | ||
| [NumTimeScales], | ||
| GpuTensorRole.Gradient); | ||
| } | ||
|
|
||
| int[] inputGradShape = batchSize == 1 | ||
| ? [numVertices, inputChannels] | ||
| : [batchSize, numVertices, inputChannels]; | ||
|
|
||
| ClearGpuCache(); | ||
|
|
||
| var inputGradTensor = new GpuTensor<T>( | ||
| backend, | ||
| inputGradBuffer, | ||
| inputGradShape, | ||
| GpuTensorRole.Gradient, | ||
| ownsBuffer: true); | ||
| inputGradBuffer = null; | ||
| return inputGradTensor; | ||
| } | ||
| finally | ||
| { | ||
| weightGradBuffer?.Dispose(); | ||
| biasGradBuffer?.Dispose(); | ||
| inputGradBuffer?.Dispose(); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Updates parameters on GPU using the configured optimizer. | ||
| /// </summary> | ||
| /// <param name="config">The GPU optimizer configuration.</param> | ||
| public override void UpdateParametersGpu(IGpuOptimizerConfig config) | ||
| { | ||
| if (Engine is not DirectGpuTensorEngine gpuEngine) | ||
| throw new InvalidOperationException("UpdateParametersGpu requires DirectGpuTensorEngine."); | ||
|
|
||
| var backend = gpuEngine.GetBackend(); | ||
| if (backend == null) | ||
| throw new InvalidOperationException("GPU backend unavailable."); | ||
|
|
||
| if (_gpuWeightsGradient == null || _gpuBiasesGradient == null || _gpuDiffusionTimesGradient == null) | ||
| throw new InvalidOperationException("BackwardGpu must be called before UpdateParametersGpu."); | ||
|
|
||
| _gpuWeights ??= new GpuTensor<T>(backend, _weights, GpuTensorRole.Weight); | ||
| _gpuBiases ??= new GpuTensor<T>(backend, _biases, GpuTensorRole.Bias); | ||
| _gpuDiffusionTimes ??= new GpuTensor<T>(backend, DiffusionTimes, [NumTimeScales], GpuTensorRole.Weight); | ||
|
|
||
| EnsureDiffusionConvOptimizerState(backend, config.OptimizerType); | ||
|
|
||
| config.ApplyUpdate( | ||
| backend, | ||
| _gpuWeights.Buffer, | ||
| _gpuWeightsGradient.Buffer, | ||
| BuildDiffusionConvOptimizerState("weights"), | ||
| _weights.Length); | ||
|
|
||
| config.ApplyUpdate( | ||
| backend, | ||
| _gpuBiases.Buffer, | ||
| _gpuBiasesGradient.Buffer, | ||
| BuildDiffusionConvOptimizerState("biases"), | ||
| _biases.Length); | ||
|
|
||
| config.ApplyUpdate( | ||
| backend, | ||
| _gpuDiffusionTimes.Buffer, |
Check notice
Code scanning / CodeQL
Block with too many statements Note
- Merge origin/master to get latest fixes including #716 diffusion conv GPU training - Fix Memory<T> indexing in DiffusionConvLayer.cs line 1493 to use .Span - Keep Memory<T> API consistency with ToFloatArray overloads Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
picks up the weightregistry dead-owner sweep (tensors #716) that fixes the foundation-scale diffusion/cv shard oom-hangs this diffusion-training pr runs into.
…ernel, not a gate) (#1745) * perf(training): gate proactive BF16-Adam on real memory pressure (keeps ≥50M models on the fused path) ShouldUseBFloat16Optimizer engaged BF16 moment storage proactively for ANY model with ≥50M parameters. But the BF16/8-bit Adam (Adam8BitOptimizer) is NOT fused-kernel- compatible — TryMapToFusedOptimizerConfig only accepts plain Adam/AdamW/SGD — so selecting it silently drops the ENTIRE model off the compiled fused-training fast path onto the eager autograd tape, ~10x slower per step. Net effect: every ≥50M-param model was training on the slow path even when it fit in memory with room to spare. Measured on ViT-Base (86.5M): the proactive BF16 forced the eager tape at ~5.0 s/step; gating it off (the model fits) keeps it on the fused path at ~3.4 s/step (~1.5x), with no change to small (<50M) models. Fix: only engage proactive BF16 when the fp32 moment state would actually consume a meaningful fraction of available memory (> 25%), mirroring the fits-in-memory guard used for weight streaming. Models that genuinely don't fit still get BF16. The reactive memory ladder (_memoryLeversForced, set on an actual OOM) is untouched and still engages BF16/8-bit on demand. AIDOTNET_BF16_ADAM=1/0 still force/disable explicitly. This is the broad half of the "memory-lever optimizers break fused training" finding in #1743. Refs #1743, #1706. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * perf(training): make BF16-Adam fused-compatible; remove the memory gate (#1745) Replaces the interim "gate proactive BF16 on memory pressure" workaround with the real fix: BF16-Adam now keeps the fused fast path instead of dropping to the eager autograd tape, so large models get BOTH the fused speed AND the halved optimizer-state footprint — no tradeoff. Pairs with AiDotNet.Tensors PR #713 (fused bf16 moment kernel + ICompiledTrainingPlan.RequestBf16MomentStorage): - Adam8BitOptimizer implements IFusedOptimizerSpec: in BFloat16 moment-storage mode it maps to the fused Adam kernel with UseBf16Moments=true. The true 8-bit block-quant mode (and adaptive-LR / AMSGrad) still has no fused kernel and correctly falls back to eager. - FusedOptimizerConfig carries UseBf16Moments; TryMapToFusedOptimizerConfig surfaces it; CompiledTapeTrainingStep calls plan.RequestBf16MomentStorage before ConfigureOptimizer so the plan allocates half-size m/v buffers. - ShouldUseBFloat16Optimizer reverts to a plain size threshold — the memory gate existed only to avoid losing the fused path, which no longer happens. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(training): Adam8Bit BF16 mode maps to fused Adam; block-quant/AMSGrad fall back (#1745) * chore(deps): bump aidotnet.tensors + native packages to 0.106.0 0.106.0 ships the fused bf16 adam moment kernel (tensors #713: adamupdatebf16simd / requestbf16momentstorage) this pr wires into compiledtapetrainingstep, plus the weightregistry dead-owner sweep (tensors #716). * fix(review): bf16-Adam config as init property, param order, changelog - FusedOptimizerConfig: move UseBf16Moments from the primary constructor to an init-only property so Deconstruct arity and positional construction sites are unchanged (only Adam8Bit sets it, now via object initializer); still part of record value equality. - TryStepWithFusedOptimizer: append useBf16Moments after eagerOptimizer instead of inserting it before, so positional call sites aren't shifted (sole caller uses named args). - Directory.Packages.props: document the 0.104.6 -> 0.106.0 bump (Tensors #713 fused bf16 moment kernel) per the file's changelog convention; note 0.106.0 is already published so CI isn't gated on an unreleased dependency. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(deps): bump aidotnet.tensors + native packages to 0.106.1 --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: franklinic <franklin@ivorycloud.com>
… step (#1748) * fix(training): persist optimizer state across checkpoints Persist tensor-backed tape optimizer state through checkpoint serialization and restore it before resumed optimizer steps. Extend the serialization coverage beyond Adam so stateful tape optimizers can resume with transient state intact. Add focused checkpoint and optimizer resume parity coverage. * fix(review): harden Adam8Bit/tape checkpoint deserialization + hooks Address the #1748 review findings on the paper-faithful Adam checkpoint path: - Adam8BitOptimizer restore hardening: bound every stream-declared length (byte/double/ushort vectors + tensor shape) against the bytes physically remaining before allocating, so a malformed checkpoint can't force an OOM; reject negative/absurd tape-state table counts, negative parameter indexes, and duplicate indexes; reject a negative tape-step (invalid bias correction / div-by-zero); validate each moment buffer's LENGTH (not just presence) against the state's element/block counts. - Adam8BitOptimizer.WriteTapeState: fail fast on a GPU-resident state instead of serializing stale host moments (the GPU step updates device buffers in place and there is no device->host readback yet) — silent checkpoint corruption. - GradientBasedOptimizerBase.RestorePendingTapeTensorStates: early-out when no pending state, so the per-parameter reflection walk no longer runs every Step(). - Tighten SerializeExtensionData/DeserializeExtensionData to private protected (same-assembly override surface only). - Checkpoint.TryRestoreOptimizer: reject restoring into a mismatched optimizer type (validate OptimizerTypeName before Deserialize). - TapeOptimizerSerializationTests: assert the cold-started Step actually updates parameters (was passing even if Step were a no-op). - sonarcloud.yml: document the 250-changed-files CodeQL upload-mode heuristic. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(review): null-safe tape step, bound ReadTapeTensor, clear stale indices, drop per-step alloc - Adam8BitOptimizer.Step: skip null parameter slots (mirror PrepareTapeState) instead of throwing in RestorePendingTapeState, while still advancing the parameter index to preserve ordering. - GradientBasedOptimizerBase.ReadTapeTensor: bound the declared rank and element count against the bytes remaining in the (seekable) checkpoint stream before allocating, so untrusted data can't force a huge allocation / OOM on restore (mirrors Adam8BitOptimizer.ReadTensor). - PrepareTapeState: clear _tapeParameterIndices before rebuilding it from the current parameters each step, so replacing a parameter set (or reusing an optimizer across models) can't retain stale tensor references or serialize state for parameters no longer in the active model. - DiffusionModelBase.Train: pass the paramTensors array straight into TapeStepContext (it implements IReadOnlyList, the ctor's type) instead of allocating a fresh List every step — keeps the tape-step path allocation-free. net10.0 + net471 build clean; 48 tape/Adam serialization tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): bump aidotnet.tensors + native packages to 0.106.0 picks up the weightregistry dead-owner sweep (tensors #716) that fixes the foundation-scale diffusion/cv shard oom-hangs this diffusion-training pr runs into. * fix(review): checkpoint overflow guards, tape-index perf, LR contract - GradientBasedOptimizerBase/Adam8BitOptimizer: checked shape-product multiply + <=int.MaxValue bound on ReadTensor so a malicious checkpoint shape can't overflow the element count and bypass the byte-remaining OOM guard. - GradientBasedOptimizerBase: skip the per-Step O(#params) tape-index rebuild in steady state (count + endpoint-reference identity check); rebuild only on pending restore or a changed parameter set. Fail fast if a derived optimizer shadows _tapeStep (name-only deserialize would restore the wrong counter). - Adam8BitOptimizer: clarify the GPU-resident-moment serialize guard (intended fail-fast vs silent stale-moment corruption; actionable workaround). - DiffusionModelBase: NumOps.ToDouble instead of Convert.ToDouble; document the once-captured fixed-LR contract + OptimizerFactory path for schedulers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(review): full tape-map verify, checkpoint FullName fallback, changelog - PrepareTapeState: replace the endpoints-only steady-state check with a full per-parameter verification — an interior slot change (or null->tensor) with unchanged first/last would otherwise return a stale index map and corrupt checkpoint indexing. Still cheaper than the rebuild (lookups, no clear/insert/ restore) in the common matching case. - Checkpoint.RestoreOptimizerState: when Type.GetType can't resolve the saved optimizer type (renamed/versioned assembly), also accept a bare FullName match so an otherwise-identical concrete type restores instead of being rejected. - Directory.Packages.props: document the 0.104.6 -> 0.106.0 bump (WeightRegistry dead-owner sweep) per the file's changelog convention. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(deps): bump aidotnet.tensors + native packages to 0.106.1 * fix(review): robust checkpoint type parsing, tape-map O(1) path, concurrency - Checkpoint: parse the saved optimizer FullName with a bracket-depth-aware scan (ExtractTypeFullName) so a closed generic's assembly-qualified name isn't truncated at commas inside [[...]]; add focused unit tests (FullName match + generic-with-commas). - GradientBasedOptimizerBase.PrepareTapeState: add an O(1) reference-identity fast path (same parameter-collection instance -> skip the verification scan), layered before the safe full verify. - GradientBasedOptimizerBase.DeserializeExtensionData: wrap reads so a truncated/ corrupt checkpoint fails with a clear InvalidOperationException, not a raw EndOfStream/IO exception. - Adam8BitOptimizer: guard the non-concurrent _pendingTapeStatesByParameterIndex with a lock (Serialize snapshots under it; Deserialize parses off-lock then swaps in) so checkpointing can't race Step/Deserialize. - DiffusionModelBase: correct the tape-step allocation comment (the two objective-re-evaluation delegates are the only per-step alloc; Adam ignores them). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(review): align Tensors changelog with the 0.106.1 rebase Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(checkpoint): net471-safe null narrowing for optimizer-type compat check net471's nullable flow analysis does not honour the string.IsNullOrEmpty guard (its BCL reference lacks [NotNullWhen]), so OptimizerTypeName was treated as possibly-null at the ExtractTypeFullName(string) call -> CS8604 (warnings as errors) broke the full-solution Build + Build-and-run-samples CI checks. Capture into a local narrowed by 'is { Length: > 0 }', which stays non-null across the block on every target framework, no null-forgiving operator. * fix(review): atomic tape-state restore/reset + validate checkpoint tables - Adam8BitOptimizer: do the pending lookup, the _tapeStates insert, and the pending remove under one _pendingTapeStatesLock (and clear both maps under the same lock in Reset). Releasing the lock between lookup and insert let a concurrent Reset() clear both maps in the gap, after which restore wrote a stale checkpoint moment into the freshly-reset optimizer. - GradientBasedOptimizerBase: validate the declared tape-step field count (0 or 1), tensor-state field count (>= 0), and per-dictionary entry count (>= 0 and not exceeding the remaining stream bytes) before allocating, and reject negative or duplicate parameter indexes. Malformed checkpoint tables are now rejected before allocation/state mutation. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: franklinic <franklin@ivorycloud.com>
Summary\n- enable GPU training for DiffusionConv with cached tensors and GPU backprop\n- auto-compute eigenbasis from Laplacian for GPU/CPU paths with optional direct CPU mode\n- mark ConvLSTM GPU training support and update GPU status docs\n\n## Testing\n- dotnet build AiDotNet.sln -c Release -v minimal (warnings: NU1608 Pomelo EFCore, CS8618 SimdBenchmark, xUnit1031 and existing nullable warnings in tests)