Skip to content

Commit ec718f3

Browse files
ooplesfranklinicclaude
authored
feat(training): centralize WGAN-GP + diffusion consumers through fused primitives (#1847)
Closes #1845 and #1846. Routes all four WGAN-GP critics through WganGpFusedStep (the fused-plan primitive from PR #1843) and adds MultiSlotFusedStep wire-ups to the three tabular diffusion consumers plus a base-class opt-in hook for DiffusionModelBase subclasses. ## Optimizer config plumbing Zero new API surface: IFusedOptimizerSpec.TryGetFusedOptimizerConfig already exposes (OptimizerType, LR, Beta1, Beta2, Epsilon, WeightDecay, Schedule, UseBf16Moments). Widened NeuralNetworkBase<T>.TryMapToFusedOptimizerConfig from private to internal so the sibling generators in the same assembly can reuse the existing helper. ## WGAN-GP consumers (#1845) * CTGANGenerator, CopulaGANGenerator, TableGANGenerator, CausalGANGenerator — each critic training method now attempts WganGpFusedStep.TryStep FIRST with the discriminator's optimizer hyperparameters extracted via TryMapToFusedOptimizerConfig, falls back to the existing GpuResidentFusedStep path (secondary fused), then the eager tape (final fallback). The ε ∈ [0, 1]^B epsilon sampler uses Engine.TensorRandomUniformRange to match each critic's local ComputeGradientPenalty behavior. * Non-Adam optimizers (Lion, LBFGS) that don't implement IFusedOptimizerSpec cleanly fall through — TryMapToFusedOptimizerConfig returns false and the code path skips to GpuResidentFusedStep as before. ## Diffusion consumers (#1846) * TabDDPMGenerator — refactored to expose a slot-based forward (BuildTabDDPMSlots + DenoiserForwardFromTensors + ComputeDiffusionLossTapeFromTensors). Per-row TrainBatch loop now attempts MultiSlotFusedStep with (numNoisy, actualNoise, catNoisy, catClean, rawSinusoidalTimeEmbed) as persistent slots. The learnable _timestepProjection stays INSIDE the compiled forward closure so its weights participate in the backward pass. Plan is compiled once on the first row and replayed via slot-data refresh for subsequent rows. * TabSynGenerator — TrainDiffusionBatch's per-row loop wired with MultiSlotFusedStep on (noisyLatent, actualNoise, projectedTimeEmbed). Matches the existing eager path's semantic that _timestepProjection is NOT in _diffMLPLayers (kept detached in the eager path too), so the projected embedding is precomputed host-side per row and passed as slot data. * Finance/Forecasting/Foundation/CSDI — - ApplyInstanceNormalization rewritten with traceable engine ops (ReduceMean + ReduceVariance + TensorSqrt + broadcast subtract/divide) — same pattern as the TFC RevIN fix. The previous `.Data.Span` per-batch loop froze at trace time. - New BuildCsdiSlots + DenoiserForwardFromSlots express the DDPM x_t formation and packed denoising input via TensorConcatenate + engine scalar multiplies. Replaces the `.Data.Span[i] = xt[0, i]` fill that baked the trace batch's x_t into the compiled plan. - Train() attempts MultiSlotFusedStep first, falls back to the existing eager ComputeDenoisingPairTape path when the fused path can't engage. * Diffusion/DiffusionModelBase — - New opt-in `protected virtual bool SupportsFusedDenoising => false;` property. Base default is false so no existing subclass changes behavior. - Train() attempts MultiSlotFusedStep when SupportsFusedDenoising is true AND the training optimizer maps cleanly to a fused config. Slots: (noisySample, noise). Loss = MSE(pred, noise). QAT shadow restoration is preserved on the fused-success path. - Subclasses with fully-traceable PredictNoise / PredictNoiseBatched (e.g. after auditing to remove `.Data.Span` host loops) can opt in via a single-line override; no infrastructure changes needed elsewhere. ## Verification * net8.0, net471, net10.0 all build clean. * No API surface changes on IGradientBasedOptimizer<T> — the existing IFusedOptimizerSpec interface (already implemented by all fuse-able optimizers) provided everything needed. * All consumers preserve eager fallback path for non-fuse-able optimizers and non-GPU hosts. Co-authored-by: franklinic <franklin@ivorycloud.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 87900aa commit ec718f3

9 files changed

Lines changed: 684 additions & 60 deletions

File tree

src/Diffusion/DiffusionModelBase.cs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,29 @@ protected virtual IEnumerable<IDisposable> EnumerateDisposableComponents()
126126
/// <inheritdoc/>
127127
public virtual ModelOptions GetOptions() => _options;
128128

129+
/// <summary>
130+
/// Opt-in flag: does this subclass's <see cref="PredictNoise"/> /
131+
/// <see cref="PredictNoiseBatched"/> path use ONLY traceable engine ops (no
132+
/// host-side <c>.Data.Span</c> loops, no per-step class-field mutation)?
133+
/// When <c>true</c>, <see cref="Train"/> attempts a
134+
/// <see cref="AiDotNet.Training.MultiSlotFusedStep{T}"/> fused-plan step
135+
/// with (noisySample, noise) as persistent slots — the compiled plan replays
136+
/// the forward per training call so <c>PredictNoise</c> must be free of
137+
/// per-call side effects that would freeze at trace time.
138+
/// <para>
139+
/// Base default is <c>false</c> so existing subclasses (which may still use
140+
/// <c>.Data.Span</c> internally) run through the eager tape as before. Override
141+
/// after auditing your forward path. See ooples/AiDotNet#1846.
142+
/// </para>
143+
/// </summary>
144+
protected virtual bool SupportsFusedDenoising => false;
145+
146+
private static void RestoreShadow(Tensor<T> param, Vector<T> shadow)
147+
{
148+
var span = param.Data.Span;
149+
for (int k = 0; k < span.Length && k < shadow.Length; k++) span[k] = shadow[k];
150+
}
151+
129152
/// <summary>
130153
/// The optional neural network architecture blueprint for custom layer configuration.
131154
/// </summary>
@@ -1132,6 +1155,70 @@ public virtual void Train(Tensor<T> input, Tensor<T> expectedOutput)
11321155
noisySampleTensor = new Tensor<T>(input._shape, noisySample);
11331156
}
11341157

1158+
// Preferred fused path: MultiSlotFusedStep with (noisySample, noise,
1159+
// timestepsPerElement) as persistent slots. Only engages when the
1160+
// concrete subclass has certified its PredictNoise/PredictNoiseBatched
1161+
// path as fully traceable by opting in via SupportsFusedDenoising —
1162+
// eager PredictNoise implementations that use host-side .Data.Span
1163+
// loops would freeze at trace time. See ooples/AiDotNet#1846.
1164+
// Only try the fused path when an optimizer has already been resolved
1165+
// (avoids double-construction with a slightly-different config).
1166+
var trainingOptimizerForFused = _trainingOptimizer;
1167+
if (SupportsFusedDenoising
1168+
&& trainingOptimizerForFused is not null
1169+
&& NeuralNetworks.NeuralNetworkBase<T>.TryMapToFusedOptimizerConfig(
1170+
trainingOptimizerForFused,
1171+
out var mfsOptType, out var mfsLr, out var mfsB1, out var mfsB2,
1172+
out var mfsEps, out var mfsWd, out _, out _))
1173+
{
1174+
var trainableForFused = CollectTrainableParameters();
1175+
var noiseSlotT = new Tensor<T>(noisySampleTensor._shape, noiseVector);
1176+
var slots = new Tensor<T>[]
1177+
{
1178+
noisySampleTensor,
1179+
noiseSlotT,
1180+
};
1181+
using var multiSlotStep = new AiDotNet.Training.MultiSlotFusedStep<T>();
1182+
var timestepsSnapshot = timesteps;
1183+
var timestepSnapshotSingle = timestep;
1184+
var isBatchedSnapshot = isBatched;
1185+
Tensor<T> ForwardFromSlots(IReadOnlyList<Tensor<T>> s)
1186+
{
1187+
return isBatchedSnapshot
1188+
? PredictNoiseBatched(s[0], timestepsSnapshot)
1189+
: PredictNoise(s[0], timestepSnapshotSingle);
1190+
}
1191+
Tensor<T> ComputeLossFromSlots(Tensor<T> pred, IReadOnlyList<Tensor<T>> s)
1192+
{
1193+
var diff = Engine.TensorSubtract(pred, s[1]);
1194+
var sq = Engine.TensorMultiply(diff, diff);
1195+
var axes = Enumerable.Range(0, sq.Shape.Length).ToArray();
1196+
return Engine.ReduceMean(sq, axes, keepDims: false);
1197+
}
1198+
if (trainableForFused.Length > 0
1199+
&& multiSlotStep.TryStep(
1200+
parameters: trainableForFused,
1201+
zeroGradAction: null,
1202+
freshSlotData: slots,
1203+
forward: ForwardFromSlots,
1204+
computeLoss: ComputeLossFromSlots,
1205+
optimizerType: mfsOptType,
1206+
learningRate: mfsLr,
1207+
beta1: mfsB1,
1208+
beta2: mfsB2,
1209+
epsilon: mfsEps,
1210+
weightDecay: mfsWd,
1211+
out T _))
1212+
{
1213+
if (qatShadows is not null && qatParams is not null)
1214+
{
1215+
for (int i = 0; i < qatParams.Length; i++)
1216+
RestoreShadow(qatParams[i], qatShadows[i]);
1217+
}
1218+
return;
1219+
}
1220+
}
1221+
11351222
using var tape = new GradientTape<T>();
11361223

11371224
// Forward pass — triggers lazy layer initialization, then we walk for

src/Finance/Forecasting/Foundation/CSDI.cs

Lines changed: 168 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -287,16 +287,51 @@ public override void Train(Tensor<T> input, Tensor<T> target)
287287

288288
var trainableParams = Training.TapeTrainingStep<T>.CollectParameters(Layers).ToArray();
289289

290-
// CSDI's stochastic denoising step cannot be traced through the compiled
291-
// fused plan on the current Tensors build. ComputeDenoisingPairTape samples
292-
// a fresh (timestep, noise) pair each call, and calling it twice — once in
293-
// the forward closure, once in the loss closure — produces two independent
294-
// samples that don't correspond to each other. Since the compiled plan
295-
// replays the SAME captured graph every step, we can't refresh the RNG
296-
// sample per replay either. This path stays on the eager tape until
297-
// Tensors' PersistentInputRegistry (PR #763) lands, at which point the
298-
// (timestep, noise) pair can be sampled per step and passed in as data.
290+
// Preferred fused path: MultiSlotFusedStep with the sampled (noise,
291+
// xt-scale, sinT) tuple passed as persistent slots. Refreshes per step
292+
// by host-sampling a fresh (t, ε) pair and copying values into the
293+
// slot tensors — the compiled forward reads the CURRENT slot data on
294+
// every replay. See ooples/AiDotNet#1846.
295+
if (trainableParams.Length > 0
296+
&& NeuralNetworks.NeuralNetworkBase<T>.TryMapToFusedOptimizerConfig(
297+
_optimizer,
298+
out var mfsOptType, out var mfsLr, out var mfsB1, out var mfsB2,
299+
out var mfsEps, out var mfsWd, out _, out _))
300+
{
301+
var slots = BuildCsdiSlots(input, target);
302+
if (slots is not null)
303+
{
304+
using var multiSlotStep = new AiDotNet.Training.MultiSlotFusedStep<T>();
305+
Tensor<T> ForwardFromSlots(IReadOnlyList<Tensor<T>> s) => DenoiserForwardFromSlots(s);
306+
Tensor<T> ComputeLossFromSlots(Tensor<T> pred, IReadOnlyList<Tensor<T>> s)
307+
{
308+
// s[1] = ε_true (noise slot). Loss = MSE(ε_pred, ε_true) via
309+
// the model's LossFunctionBase so custom losses are respected.
310+
return loss.ComputeTapeLoss(pred, s[1]);
311+
}
312+
if (multiSlotStep.TryStep(
313+
parameters: trainableParams,
314+
zeroGradAction: null,
315+
freshSlotData: slots,
316+
forward: ForwardFromSlots,
317+
computeLoss: ComputeLossFromSlots,
318+
optimizerType: mfsOptType,
319+
learningRate: mfsLr,
320+
beta1: mfsB1,
321+
beta2: mfsB2,
322+
epsilon: mfsEps,
323+
weightDecay: mfsWd,
324+
out T fusedLoss))
325+
{
326+
LastLoss = fusedLoss;
327+
return;
328+
}
329+
}
330+
}
299331

332+
// Eager fallback: samples (t, ε) inline and runs the denoising pair on
333+
// the tape. Same objective; used when the fused path can't engage
334+
// (non-fuse-able optimizer, non-GPU host, etc.).
300335
using var tape = new GradientTape<T>();
301336
var (epsilonPred, epsilonTarget) = ComputeDenoisingPairTape(input, target);
302337

@@ -352,6 +387,112 @@ public override void Train(Tensor<T> input, Tensor<T> target)
352387
}
353388
}
354389

390+
/// <summary>
391+
/// Assembles the persistent-slot data for the MultiSlotFusedStep wire-up:
392+
/// samples (t, ε) host-side then packs everything the compiled forward
393+
/// needs as tensor slots. Slot layout:
394+
/// <list type="bullet">
395+
/// <item><description>[0] target — the clean values to denoise (Ho 2020 x_0).</description></item>
396+
/// <item><description>[1] ε_true — freshly-sampled Gaussian noise, same shape as target.</description></item>
397+
/// <item><description>[2] sqrtAlphaBar_scalar — cumulative α scaling at the sampled t, shape [1].</description></item>
398+
/// <item><description>[3] sqrtOneMinusAlphaBar_scalar — noise scaling at t, shape [1].</description></item>
399+
/// <item><description>[4] sinT_scalar — sin(2π·t/T) timestep encoding, shape [1].</description></item>
400+
/// <item><description>[5] conditioned — RevIN-normalized observed input, shape [1, seqLen].</description></item>
401+
/// </list>
402+
/// Returns null when the target is degenerate (zero length) so the caller
403+
/// falls back to eager training.
404+
/// </summary>
405+
private IReadOnlyList<Tensor<T>>? BuildCsdiSlots(Tensor<T> input, Tensor<T> target)
406+
{
407+
int targetLen = target.Length;
408+
if (targetLen <= 0) return null;
409+
410+
var rand = RandomHelper.CreateSecureRandom();
411+
int t = rand.Next(_numDiffusionSteps);
412+
var noiseData = new T[targetLen];
413+
for (int i = 0; i < targetLen; i++) noiseData[i] = SampleStandardNormal(rand);
414+
var epsilonTrue = new Tensor<T>(target._shape, new Vector<T>(noiseData));
415+
416+
T sqrtAlphaBar = NumOps.Sqrt(_alphasCumprod[t]);
417+
T sqrtOneMinus = NumOps.Sqrt(NumOps.Subtract(NumOps.One, _alphasCumprod[t]));
418+
T sinT = NumOps.FromDouble(Math.Sin(2.0 * Math.PI * t / Math.Max(1, _numDiffusionSteps - 1)));
419+
420+
var sqrtAlphaBarT = new Tensor<T>(new[] { 1 });
421+
sqrtAlphaBarT[0] = sqrtAlphaBar;
422+
var sqrtOneMinusT = new Tensor<T>(new[] { 1 });
423+
sqrtOneMinusT[0] = sqrtOneMinus;
424+
var sinTT = new Tensor<T>(new[] { 1 });
425+
sinTT[0] = sinT;
426+
427+
var conditioned = ApplyInstanceNormalization(input);
428+
if (conditioned.Rank == 1)
429+
conditioned = Engine.Reshape(conditioned, new[] { 1, conditioned.Length });
430+
return new[] { target, epsilonTrue, sqrtAlphaBarT, sqrtOneMinusT, sinTT, conditioned };
431+
}
432+
433+
/// <summary>
434+
/// Fused-path denoiser forward: reads the slot tuple from
435+
/// <see cref="BuildCsdiSlots"/>, builds x_t via traceable engine
436+
/// arithmetic (no <c>.Data.Span</c> pack — the previous inline
437+
/// implementation froze the trace batch's x_t into the compiled plan),
438+
/// concatenates [x_t | conditioning_slice | sin(t)] via
439+
/// <see cref="IEngine.TensorConcatenate{T}"/>, and runs the projection +
440+
/// residual stack + output projection. Returns predicted noise aligned
441+
/// with the noise slot's shape.
442+
/// </summary>
443+
private Tensor<T> DenoiserForwardFromSlots(IReadOnlyList<Tensor<T>> slots)
444+
{
445+
var target = slots[0];
446+
var epsilonTrue = slots[1];
447+
var sqrtAlphaBarT = slots[2];
448+
var sqrtOneMinusT = slots[3];
449+
var sinTT = slots[4];
450+
var conditioned = slots[5];
451+
452+
int targetLen = target.Length;
453+
// x_t = sqrt(α̅_t) * target + sqrt(1-α̅_t) * ε — all traceable.
454+
var scaledTarget = Engine.TensorMultiplyScalar(target, sqrtAlphaBarT[0]);
455+
var scaledNoise = Engine.TensorMultiplyScalar(epsilonTrue, sqrtOneMinusT[0]);
456+
var xt = Engine.TensorAdd(scaledTarget, scaledNoise);
457+
var xt1d = xt.Rank == 1 ? xt : Engine.Reshape(xt, new[] { targetLen });
458+
459+
// Conditioning slice: first Min(condLen, hiddenDimension) elements,
460+
// flattened to 1-D so we can concat with xt1d + sinT.
461+
var condFlat = conditioned.Rank == 1
462+
? conditioned
463+
: Engine.Reshape(conditioned, new[] { conditioned.Length });
464+
int condLen = Math.Min(condFlat.Length, _hiddenDimension);
465+
var condSlice = condLen == condFlat.Length
466+
? condFlat
467+
: Engine.TensorSlice(condFlat, new[] { 0 }, new[] { condLen });
468+
469+
// Pack [xt | conditioning | sin(t)] via TensorConcatenate — replaces
470+
// the .Data.Span[i] fill that froze at trace time.
471+
var packed1D = Engine.TensorConcatenate(new[] { xt1d, condSlice, sinTT }, axis: 0);
472+
var denoisingInput = Engine.Reshape(packed1D, new[] { 1, targetLen + condLen + 1 });
473+
474+
var eps = (Tensor<T>)denoisingInput;
475+
if (_inputProjection is not null)
476+
eps = _inputProjection.Forward(eps);
477+
foreach (var layer in _residualLayers)
478+
eps = layer.Forward(eps);
479+
if (_outputProjection is not null)
480+
eps = _outputProjection.Forward(eps);
481+
482+
if (eps.Rank == 2 && eps.Shape[1] > epsilonTrue.Length)
483+
eps = Engine.TensorNarrow(eps, dim: 1, start: 0, length: epsilonTrue.Length);
484+
if (eps.Length != epsilonTrue.Length)
485+
{
486+
throw new InvalidOperationException(
487+
$"CSDI denoising pair: predicted-noise length ({eps.Length}, shape=["
488+
+ $"{string.Join(",", eps._shape)}]) does not match true-noise length ("
489+
+ $"{epsilonTrue.Length}, shape=[{string.Join(",", epsilonTrue._shape)}]).");
490+
}
491+
if (!eps._shape.AsEnumerable().SequenceEqual(epsilonTrue._shape))
492+
eps = Engine.Reshape(eps, epsilonTrue._shape);
493+
return eps;
494+
}
495+
355496
/// <summary>
356497
/// Builds the (predicted-noise, true-noise) pair for one DDPM
357498
/// training step. Samples a timestep and a fresh noise tensor,
@@ -535,23 +676,28 @@ public override Dictionary<string, T> Evaluate(Tensor<T> predictions, Tensor<T>
535676
return new Dictionary<string, T> { ["MSE"] = mse, ["MAE"] = mae, ["RMSE"] = NumOps.Sqrt(mse) };
536677
}
537678

679+
/// <inheritdoc/>
680+
/// <remarks>
681+
/// Traceable RevIN forward — same pattern as
682+
/// <see cref="TFC{T}.ApplyInstanceNormalization"/>. Uses
683+
/// <see cref="IEngine.ReduceMean{T}"/> / <see cref="IEngine.ReduceVariance{T}"/>
684+
/// / <see cref="IEngine.TensorSqrt{T}"/> / broadcast subtract+divide so the
685+
/// per-batch stats re-execute on every replay under a compiled fused plan.
686+
/// </remarks>
538687
public override Tensor<T> ApplyInstanceNormalization(Tensor<T> input)
539688
{
540689
int batchSize = input.Rank > 1 ? input.Shape[0] : 1;
541690
int seqLen = input.Rank > 1 ? input.Shape[1] : input.Length;
542-
var result = new Tensor<T>(input._shape);
543-
for (int b = 0; b < batchSize; b++)
544-
{
545-
T mean = NumOps.Zero;
546-
for (int t = 0; t < seqLen; t++) { int idx = b * seqLen + t; if (idx < input.Length) mean = NumOps.Add(mean, input[idx]); }
547-
mean = NumOps.Divide(mean, NumOps.FromDouble(seqLen));
548-
T variance = NumOps.Zero;
549-
for (int t = 0; t < seqLen; t++) { int idx = b * seqLen + t; if (idx < input.Length) { var diff = NumOps.Subtract(input[idx], mean); variance = NumOps.Add(variance, NumOps.Multiply(diff, diff)); } }
550-
variance = NumOps.Divide(variance, NumOps.FromDouble(seqLen));
551-
T std = NumOps.Sqrt(NumOps.Add(variance, NumOps.FromDouble(1e-5)));
552-
for (int t = 0; t < seqLen; t++) { int idx = b * seqLen + t; if (idx < input.Length && idx < result.Length) result.Data.Span[idx] = NumOps.Divide(NumOps.Subtract(input[idx], mean), std); }
553-
}
554-
return result;
691+
if (seqLen <= 0) return input;
692+
693+
bool reshaped = input.Rank != 2;
694+
var flat = reshaped ? Engine.Reshape(input, new[] { batchSize, seqLen }) : input;
695+
var mean = Engine.ReduceMean(flat, new[] { 1 }, keepDims: true);
696+
var variance = Engine.ReduceVariance(flat, new[] { 1 }, keepDims: true);
697+
var std = Engine.TensorSqrt(Engine.TensorAddScalar(variance, NumOps.FromDouble(1e-5)));
698+
var centered = Engine.TensorBroadcastSubtract(flat, mean);
699+
var normalized = Engine.TensorBroadcastDivide(centered, std);
700+
return reshaped ? Engine.Reshape(normalized, input._shape) : normalized;
555701
}
556702

557703
public override Dictionary<string, T> GetFinancialMetrics()

src/NeuralNetworks/NeuralNetworkBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8800,7 +8800,7 @@ private static void EmitFusedPathEventIfEnabled(bool hit, string? reason)
88008800
/// configure-once contract: adaptive learning rates, attached LR schedulers,
88018801
/// or AMSGrad mode (which the fused kernel doesn't model).
88028802
/// </summary>
8803-
private static bool TryMapToFusedOptimizerConfig(
8803+
internal static bool TryMapToFusedOptimizerConfig(
88048804
IGradientBasedOptimizer<T, Tensor<T>, Tensor<T>> optimizer,
88058805
out AiDotNet.Tensors.Engines.Compilation.OptimizerType optimizerType,
88068806
out float learningRate,

src/NeuralNetworks/SyntheticData/CTGANGenerator.cs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,46 @@ private void TrainDiscriminatorStepBatched(Matrix<T> transformedData, int numPac
614614

615615
var (realPacked, fakePacked) = BuildPackedRealAndFakeBatches(transformedData, numPacks);
616616

617-
// GPU-RESIDENT WGAN-GP disc via Tensors PR #763.
617+
// Preferred fused path: WganGpFusedStep runs the full WGAN-GP objective
618+
// (Wasserstein + λ·GP with createGraph=true GP) in one compiled plan,
619+
// with persistent (real, fake, ε) slots refreshed each Step. Falls
620+
// through to the GpuResidentFusedStep path (which uses this critic's
621+
// per-batch epsilon sampled inline) when the optimizer has no fused-
622+
// kernel mapping (adaptive LR, non-fuse-able type) or the primitive
623+
// itself declines. See ooples/AiDotNet#1845.
624+
var discParams = TapeTrainingStep<T>.CollectParameters(_discLayers);
625+
if (discParams.Count > 0
626+
&& NeuralNetworks.NeuralNetworkBase<T>.TryMapToFusedOptimizerConfig(
627+
_discriminatorOptimizer,
628+
out var wganOptType, out var wganLr, out var wganB1,
629+
out var wganB2, out var wganEps, out var wganWd,
630+
out _, out _))
631+
{
632+
using var wganStep = new AiDotNet.Training.WganGpFusedStep<T>();
633+
Tensor<T> DiscFwd(Tensor<T> inp) => DiscriminatorForwardBatched(inp, isTraining: true);
634+
Tensor<T> EpsilonSampler(int bs) =>
635+
Engine.TensorRandomUniformRange<T>(new[] { bs, 1 }, NumOps.Zero, NumOps.One);
636+
if (wganStep.TryStep(
637+
discParameters: discParams,
638+
realBatch: realPacked,
639+
fakeBatch: fakePacked,
640+
discForward: DiscFwd,
641+
epsilonSampler: EpsilonSampler,
642+
gradientPenaltyWeight: _options.GradientPenaltyWeight,
643+
optimizerType: wganOptType,
644+
learningRate: wganLr,
645+
beta1: wganB1,
646+
beta2: wganB2,
647+
epsilon: wganEps,
648+
weightDecay: wganWd,
649+
out T _))
650+
{
651+
return;
652+
}
653+
}
654+
655+
// Secondary fused path: GpuResidentFusedStep with the loss composed via
656+
// this class's ComputeGradientPenalty (createGraph=true GP fix, #1844).
618657
var trainableDiscLayers = _discLayers.OfType<ITrainableLayer<T>>().ToList();
619658
if (trainableDiscLayers.Count > 0)
620659
{
@@ -650,7 +689,8 @@ Tensor<T> Loss(Tensor<T> allScores, Tensor<T> _)
650689
}
651690

652691
using var tape = new GradientTape<T>();
653-
var discParams = TapeTrainingStep<T>.CollectParameters(_discLayers);
692+
// discParams already collected above for the WganGpFusedStep attempt;
693+
// reuse it here to avoid a redundant Layers → parameter scan.
654694

655695
var realScores = DiscriminatorForwardBatched(realPacked, isTraining: true);
656696
var fakeScores = DiscriminatorForwardBatched(fakePacked, isTraining: true);

0 commit comments

Comments
 (0)