Uh oh!
There was an error while loading. Please reload this page.
feat(layers): migrate cached-for-backward intermediates to SaveForBackward / live-input recompute (ADR 006, T2.3) - #848
Merged
Conversation
Pulls graph.Saver/graph.SaverAware and the arena Pin/Unpin lifecycle (zerfoo/ztensor#132, SHA 1cc7793f9acc) needed for the T2.3 migration. Refs #847, zerfoo/ztensor#128.
…intermediates (ADR 006, T2.3) - BaseActivation, Gelu, LeakyReLU: Backward recomputes the derivative from the live inputs the graph passes in; the lastInput struct-field cache is removed (arena could reuse it before Backward, #842 bug class). - Sigmoid, Softmax, SwiGLU: implement graph.SaverAware; the cached forward outputs consumed by Backward (sigmoid output, softmax output, gate and silu(x1)) are registered via SaveForBackward so arena-backed storage stays pinned until Backward. SwiGLU re-splits the live input instead of caching it; the Saver fans out to the composed Sigmoid. - Regression tests: stub-Saver SAVE assertions for Softmax and SwiGLU; live-input perturbation RECOMPUTE proof for Gelu. Refs #847, zerfoo/ztensor#128.
…Norm SaveForBackward (ADR 006, T2.3) - LayerNormalization: the generic engine-op Backward (used for f64 / non-mixed types) now recomputes mean/variance/normedInput from the live inputs, completing the #842 fix that previously covered only the mixed-precision f32 path. The mean/variance/normedInput struct-field caches are removed. - RMSNorm, SimplifiedLayerNormalization: Backward recomputes the RMS statistics from live inputs via the new rmsRecomputeStats helper; inputTensor/rms/invStdDev/normalizedInput caches removed. This also fixes the fused-GPU forward path that could leave rsqrt nil. - BatchNormalization: Backward only receives X (not scale/mean/var), so its normalized/std/scaleBC caches cannot be recomputed from live inputs; the layer now implements graph.SaverAware and registers them via SaveForBackward. - Regression tests: live-input perturbation proofs for both LayerNorm backward paths; cache-dependency tests updated to the new semantics (backward-before-forward now succeeds for recompute layers). Refs #847, zerfoo/ztensor#128.
… 006, T2.3) Dropout and FeatureDropout masks are random and cannot be recomputed in Backward, so they are the canonical SAVE case: both layers implement graph.SaverAware and register the mask every training-mode Forward, pinning arena-backed storage until Backward consumes it. Stub-Saver regression tests assert the mask is registered in training mode and nothing is registered in eval mode. Refs #847, zerfoo/ztensor#128.
…d (ADR 006, T2.3) ScaledDotProductAttention is not itself a graph.Node (owning nodes call its Backward with nil q/k/v), so its cached q/k/v and attention weights are SAVE-class intermediates: SDPA gains SetSaver and registers them during Forward. AttentionHead, GroupedQueryAttention, GlobalAttention, LocalAttention, and MultiHeadLatentAttention implement graph.SaverAware and fan the graph-provided Saver into their composed SDPA/GQA; GQA additionally saves attnOutputFinal (read by wo backward) and MLA saves its compressed-KV latent and pre-wO attention output. FusedSDPA's hadMask is a host bool (SAFE, no change). Refs #847, zerfoo/ztensor#128.
…SN/Polynomial (ADR 006, T2.3) - FFN: Backward reads the original input from the live inputs the graph passes in (inputTensor cache removed); w1Output/w3Output/swiGLUOutput are matmul outputs (expensive) and are registered via SaveForBackward. The Saver fans into the composed SwiGLU. - FiLM: the feature tensor is read from live inputs; the generated scale (a Dense forward output) is registered via SaveForBackward; the unused lastFeature/lastBias caches are removed. - TemporalConvEncoder: conv1/conv2/pooled outputs registered via SaveForBackward. - VariableSelection: hidden activation and selection weights registered via SaveForBackward; unread lastInput cache removed. - PolynomialExpansion: Backward now requires and reads the live input; the stale-cache-first preference (cache over live inputs) is removed. MatMulNBits (persistent dequantized-weight cache), MixtureOfExperts and MoEGate are classified as FOLLOW-UP in the T2.3 audit table. Refs #847, zerfoo/ztensor#128.
…seeding the cache (ADR 006, T2.3) Gelu's Backward now recomputes from the live inputs it receives; the forward-seed step is unnecessary and the cached-input path no longer exists. This also removes a wasted full GELU forward per backward call. Refs #847, zerfoo/ztensor#128.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements plan task T2.3 of
docs/plan-gpu-training-hardening.md: a field-based audit of everygraph.Node/ layerBackwardin zerfoo for struct fields written duringForwardand read duringBackward, plus migration of the training-critical set to the ztensor ADR 006 contract (graph.SaverAware/SaveForBackward, landed in zerfoo/ztensor#132 @1cc7793) or to live-inputs ...recompute.Refs #847, zerfoo/ztensor#128.
Why
Nodes caching forward intermediates in struct fields and reading them in Backward are corrupted by GPU arena reuse before Backward runs (#842 LayerNorm variance, #845, the Wolf QK-norm cached inverse). The audit is AST-based (receiver fields assigned in Forward ∩ read in Backward, following same-receiver helper calls), not comment-based.
Audit method
A
go/astwalker over every non-test.gofile: for each type with pointer-receiverForwardandBackward, intersect {fields assigned in Forward} with {fields read in Backward} (transitively through same-receiver method calls). 45 types flagged; every one is classified below.Audit table
Classification: SAFE = host Go value, no device/arena storage. RECOMPUTE = derived from the live
inputs ...Backward receives (preferred for cheap intermediates; the #842 LayerNorm pattern). SAVE = expensive to recompute; registered viaSaveForBackward(node keeps the Go handle, the contract owns the memory lifetime).Migrated in this PR (training-critical path: core, activations, normalization, regularization, attention, transpose, reducesum)
activations.BaseActivation(ReLU/Tanh/Erf/…)lastInputinputs[0]activations.GelulastInputinputs[0];OutputShapefrom host[]intactivations.LeakyReLUlastInputinputs[0]activations.SigmoidlastOutput(lastInputwas dead)SaverAware; output registered each Forward; deadlastInputremovedactivations.SoftmaxoutputSaverAware; softmax output registered each Forwardactivations.SwiGLUlastInput;gate,siluX1inputs[0]; gate/silu registered; Saver fans into composed Sigmoidnormalization.LayerNormalizationmean,variance,normedInput,inputShapeinputs[0]like the mixed f32 path; caches removednormalization.RMSNorminputTensor,rmsrmsRecomputeStatshelper; also fixes fused-GPU forward leavingrsqrtnilnormalization.SimplifiedLayerNormalizationinvStdDev,normalizedInputnormalization.BatchNormalizationnormalized,std,scaleBCSaverAware+ register all threeregularization.DropoutmaskSaverAware+ register (training mode only)regularization.FeatureDropoutmaskattention.ScaledDotProductAttentionq,k,v,attentionWeightsgraph.Node(owners call Backward with nil q/k/v); gainsSetSaver; owners fan their Saver in so saves attribute to the owning nodeattention.AttentionHeadSaverAware; fans into SDPAattention.GroupedQueryAttentionattnOutputFinalSaverAware; registersattnOutputFinal(read bywobackward) + fans into SDPAattention.GlobalAttention,attention.LocalAttentionSaverAware; fan into GQAattention.MultiHeadLatentAttentioncachedCKV,cachedAttnOutSaverAware; registers both + fans into SDPAattention.FusedSDPAhadMaskcore.FFNinputTensor;w1Output,w3Output,swiGLUOutputinputs[0](signature previously ignored them); matmul outputs registered; Saver fans into composed SwiGLUcore.FiLMlastFeature,lastScale(lastBiasdead)inputs[0]; generated scale registered; dead caches removedcore.PolynomialExpansionlastInputcore.TemporalConvEncoderlastConv1Out,lastConv2Out,lastPooledSaverAware+ registercore.VariableSelectionlastHidden,lastWeights(lastInputdead)SaverAware+ register; dead cache removedcore.RotaryEmbeddinginnertranspose.Transposeperm[]intlazily defaulted in Forwardreducesum.ReduceSumfunctional.GELUBackwardGelu.Backwardinstead of forward-seeding the removed cache (also removes a wasted forward per call)core.Linear/Bias/Add/MatMul/Dense/Gemmtimeseries/*,layers/timeseries/*)FOLLOW-UP (classified, not migrated here; tracked under #847)
core.MatMulNBitsdequantizedWeights,cacheValidcore.MixtureOfExpertscachedAssignments/ExpertOuts/GateWeight/HiddenStates/Indices/Weightscore.MoEGatecachedGateWeight/HiddenStates/Indices/Probs(cachedNumExpertsSAFE int)embeddings.RotaryPositionalEmbeddingcosAngles,sinAnglesembeddings.TokenEmbeddinginputTokenIDshrm.HModule,hrm.LModulefwdCombinedInput(shape/bool fields SAFE)recurrent.SimpleRNNlastInput(RECOMPUTE);hiddenStatessm.BCNormcachedInput,cachedNormssm.MambaBlock/ssm.MIMOMambaBlock/ssm.ComplexSSMStatetransformer.BlockfwdInput(RECOMPUTE);fwdNorm1Out/fwdNorm2Out/fwdPostAttn/fwdResidual1training/fp8.FP8LinearlastInputtraining/lora.LoraLinearlastAxtraining/loss.MSE/BCELoss/CorrLoss/RoutingContrastivepredictions,targets,scorestraining/loss.CrossEntropyLosspredictions,targets(RECOMPUTE);softmaxOutputtraining/nas.DARTSLayerlastInput(RECOMPUTE);opOutputs,weightsCounts: 45 types flagged ⇒ 27 resolved in this PR (11 RECOMPUTE migrations, 15 SAVE registrations/fan-outs, plus SAFE documentation), 18 FOLLOW-UP types classified above. Re-running the audit on the migrated tree shows the prioritized packages contain only SAVE-registered or SAFE fields.
Wiring
ztensor
Builder.Build(1cc7793) callsSetSaveron everySaverAwarenode, so anything constructed through the graph builder (zerfooinference/arch_*, Wolf's CrossAsset graph) is wired automatically. Composite layers fan the Saver into children (SwiGLU→Sigmoid,FFN→SwiGLU,AttentionHead/GQA/MLA→SDPA,Global/Local→GQA) so child saves attribute to the parent node and release when the parent's Backward returns. Nodes used outside a Graph tolerate a nil Saver (no-op), keeping CPU behavior identical.Regression tests
TestDropout_SaveForBackward_RegistersMask/TestFeatureDropout_…(mask registered in training mode, nothing in eval),TestSoftmax_SaveForBackward_RegistersOutput,TestSwiGLU_SaveForBackward_RegistersGateAndSilu— stubgraph.Saverrecording exactly which tensors are registered.TestGelu_Backward_ReadsLiveInputs,TestLayerNorm_MixedBackward_ReadsLiveInput(f32),TestLayerNorm_GenericBackward_ReadsLiveInput(f64, the path newly migrated here) — the input buffer is overwritten in place between Forward and Backward (exactly what arena reuse does) and gradients must match a fresh layer that only ever saw the new values.Gates
go build ./...✓go vet ./...✓go test -race -count=1✓ on all changed packages (layers/activations,layers/normalization,layers/regularization,layers/attention,layers/core,layers/functional)go test ./... -count=1full suite ✓ exceptcmd/benchTestBenchHarness, which is pre-existing flaky (fails identically with-count=5on an untouchedmaincheckout; timing-basedThroughputTs > 0assertion)Contract gaps found (feeding ztensor#132 open questions)
MatMulNBits.dequantizedWeights,SimpleRNN.hiddenState): the contract releases pins when the node's Backward returns, but these caches must survive across steps until invalidated. Needs either a pin-until-invalidate API or a guarantee that such caches are allocated outside the arena. This is related to ztensor#132 open question 2 (realloc between save and backward).MarkStepBoundaryrelease hook (ztensor#132 open question 3): not needed by any layer migrated here — next-Forward + end-of-Backward release covers every zerfoo training/inference loop encountered.Backwardwithout passing inputs, relying on caches (functional.GELUBackward, FFN tests). The graph always passes live inputs, but a lint (vet check or contract doc) that Backward implementations must not depend on Forward-time tensor state would prevent regressions; the gradcheck-under-poison harness (T1.2/S2.3.1) is the runtime backstop.Not done here