@@ -6141,6 +6141,22 @@ private TrainSentinel AcquireTrainSentinel()
61416141 /// </summary>
61426142 public StreamingTrainingMode StreamingTraining { get; set; } = StreamingTrainingMode.Auto;
61436143
6144+ /// <summary>
6145+ /// #1662 lever #1 (§5c) — opt-in single-pass approximate gradient clipping for the streaming
6146+ /// (optimizer-in-backward) path. When <c>true</c> and clipping is active, the clip scale is
6147+ /// computed from an EMA of the PREVIOUS step's global grad-norm instead of the exact current
6148+ /// norm, so even clipped training runs in ONE backward pass (no 2× norm pass). This is an
6149+ /// approximation (NFNet-style adaptive clipping, Brock et al. 2021): it is NOT bit-identical
6150+ /// to exact <c>clip_grad_norm_</c> and changes the trajectory. Default: <c>false</c> (exact
6151+ /// two-pass). Only meaningful when streaming is engaged (e.g. <see cref="StreamingTrainingMode.ForceOn"/>).
6152+ /// </summary>
6153+ public bool FastApproxGradClip { get; set; } = false;
6154+
6155+ // EMA of the global grad-norm for FastApproxGradClip; < 0 until seeded on the first clipped
6156+ // fast-clip step. Persists across steps so the estimate tracks the run.
6157+ private double _fastClipEmaNorm = -1.0;
6158+ private const double FastClipEmaBeta = 0.9;
6159+
61446160 /// <summary>Learning rate used by the default streaming 8-bit Adam epilogue. Defaults
61456161 /// to 1e-4 — the conservative rate large-transformer/foundation-model
61466162 /// training uses, which stays stable under 8-bit moment quantization. When
@@ -6267,17 +6283,21 @@ protected void TrainWithTapeStreaming(
62676283 // clipping is off, one streaming pass suffices and the tape is non-persistent.
62686284 double maxGradNorm = MaxGradNormValue;
62696285 bool clip = maxGradNorm > 0.0;
6286+ // #1662 lever #1 (§5c): opt-in fast-clip turns the clipped case into a SINGLE streaming
6287+ // pass by clipping with an EMA of the previous step's global grad-norm (NFNet-style
6288+ // adaptive clipping) instead of the exact current-step norm. Only the exact path needs
6289+ // the two-pass-over-a-persistent-tape dance; fast-clip and unclipped are single-pass.
6290+ bool twoPass = clip && !FastApproxGradClip;
62706291
62716292 using var tape = new GradientTape<T>(
6272- clip ? new GradientTapeOptions { Persistent = true } : null);
6273- // #1662 lever #1: the clipped two-pass path reuses the persistent tape across the norm
6274- // pass and the apply pass. ComputeGradientsStreaming releases activations by default
6275- // (the process-global GradientTape<T>.ReleaseStreamingActivations), which would make the
6276- // second pass throw "activations released" — so for the clipped path keep them resident,
6277- // saving/restoring the flag so the setting never leaks to other tapes/steps. (The
6278- // unclipped single-pass path keeps the default release, freeing each grad as produced.)
6293+ twoPass ? new GradientTapeOptions { Persistent = true } : null);
6294+ // The exact-clip two-pass path reuses the persistent tape across the norm pass and the
6295+ // apply pass. ComputeGradientsStreaming releases activations by default (the process-
6296+ // global GradientTape<T>.ReleaseStreamingActivations), which would make the second pass
6297+ // throw "activations released" — so for that path keep them resident, saving/restoring
6298+ // the flag so the setting never leaks. (Single-pass paths keep the default release.)
62796299 bool savedStreamingRelease = GradientTape<T>.ReleaseStreamingActivations;
6280- if (clip ) GradientTape<T>.ReleaseStreamingActivations = false;
6300+ if (twoPass ) GradientTape<T>.ReleaseStreamingActivations = false;
62816301 try
62826302 {
62836303 var output = ForwardForTraining(input);
@@ -6341,7 +6361,7 @@ protected void TrainWithTapeStreaming(
63416361 streamingOptimizer.Apply(source, grad);
63426362 });
63436363 }
6344- else
6364+ else if (twoPass)
63456365 {
63466366 // Clip set = layer-owned trainable params only (NOT the extras), matching
63476367 // the eager ApplyGradientClipping call site exactly so the global norm and
@@ -6391,6 +6411,48 @@ protected void TrainWithTapeStreaming(
63916411 streamingOptimizer.Apply(source, grad);
63926412 });
63936413 }
6414+ else
6415+ {
6416+ // #1662 lever #1 (§5c) FAST-CLIP — opt-in, NOT bit-identical. Clip with an EMA of the
6417+ // PREVIOUS step's global grad-norm so the whole step is a SINGLE streaming pass even
6418+ // when clipping (no 2× backward). The exact current-step norm is accumulated in the
6419+ // same pass and folded into the EMA for next step. First step (no EMA): don't clip,
6420+ // just seed. NFNet-style adaptive clipping (Brock et al. 2021); documented approximation.
6421+ var clipSet = new HashSet<Tensor<T>>(
6422+ trainableParams, Helpers.TensorReferenceComparer<Tensor<T>>.Instance);
6423+
6424+ double emaNorm = _fastClipEmaNorm; // < 0 until seeded
6425+ bool haveEma = emaNorm >= 0.0;
6426+ bool scaleDown = haveEma && emaNorm > maxGradNorm;
6427+ T scale = scaleDown
6428+ ? NumOps.FromDouble(maxGradNorm / (emaNorm + 1e-6))
6429+ : NumOps.One;
6430+
6431+ double curNormSq = 0.0;
6432+ tape.ComputeGradientsStreaming(lossTensor, sources,
6433+ (source, grad) =>
6434+ {
6435+ if (grad is null || grad.Length == 0) return;
6436+ if (clipSet.Contains(source))
6437+ {
6438+ var span = grad.Data.Span;
6439+ int len = grad.Length;
6440+ for (int i = 0; i < len; i++)
6441+ {
6442+ double v = NumOps.ToDouble(span[i]); // unscaled — feeds the true-norm EMA
6443+ curNormSq += v * v;
6444+ if (scaleDown) span[i] = NumOps.Multiply(span[i], scale);
6445+ }
6446+ }
6447+ streamingOptimizer.Apply(source, grad);
6448+ });
6449+
6450+ // Fold this step's exact norm into the EMA for the next step's clip estimate.
6451+ double curNorm = Math.Sqrt(curNormSq);
6452+ _fastClipEmaNorm = haveEma
6453+ ? FastClipEmaBeta * emaNorm + (1.0 - FastClipEmaBeta) * curNorm
6454+ : curNorm;
6455+ }
63946456
63956457 // Post-step hook: first-order optimizers already updated each parameter in place during
63966458 // Apply (no-op here); a full-gradient optimizer like streaming L-BFGS buffered the
@@ -6409,7 +6471,7 @@ protected void TrainWithTapeStreaming(
64096471 }
64106472 finally
64116473 {
6412- if (clip ) GradientTape<T>.ReleaseStreamingActivations = savedStreamingRelease;
6474+ if (twoPass ) GradientTape<T>.ReleaseStreamingActivations = savedStreamingRelease;
64136475 }
64146476 }
64156477
0 commit comments