Skip to content

Commit 7be00cd

Browse files
ooplesclaude
andauthored
perf(timeseries): stop per-element GPU host reads in Autoformer/Informer/TFT forwards (#1897)
These three sequence models read GPU-resident tensors ELEMENT BY ELEMENT inside their forward passes. On the DirectGpu engine each such index is a separate device->host transfer that flushes the pipeline, so a single forward stalled the GPU hundreds to thousands of times. Two sites, both on the hot path: 1. Autoformer AutoCorrelationEngine top-k selection read the [corrLen] correlation vector one element at a time (corrLen syncs per call, x4 call sites per forward: encoder self-attn x2, decoder self+cross). Now downloads it once via GetCpuData(). 2. All three assemble the per-window/per-batch positional encoding by indexing the _positionalEncoding tensor per element every forward (seqLen*dim for Autoformer, batch*seqLen*dim for Informer and TFT). The PE is CONSTANT after construction, so it is now downloaded to a host array once and cached (refreshed on deserialize). Behavior is unchanged - same values, same gradients; the correlation top-k choice was already non-differentiable and the PE was already a constant, no-grad input. This is purely about how the values reach the host. Measured on an RTX 3080 (FCHL research sweep, float models, 15-min bars): GPU utilization during heavy-transformer training: ~7% -> 10-15% HONEST SCOPE - what this does NOT fix: these models were also timing out in that sweep, and this change does NOT resolve that. That turned out not to be a code defect at all: one research cell runs 7 full trainings (6 walk-forward folds + 1 hold-out refit), and at 8 epochs that needs ~300-520s against the harness's 300s cap. Measured cost is ~9.3s per training at 1 epoch; given an adequate budget the same cells complete with 0 timeouts and 0 faults. So this PR is a genuine but incremental GPU-efficiency fix, not a fix for the timeouts. Builds clean on net10.0, net8.0 and net471. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 259d58b commit 7be00cd

3 files changed

Lines changed: 29 additions & 6 deletions

File tree

src/TimeSeries/AutoformerModel.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ public class AutoformerModel<T> : TimeSeriesModelBase<T>, ISupportsLossFunction<
107107
// Input embedding
108108
private Tensor<T> _inputProjection; // [embeddingDim, 1]
109109
private Tensor<T> _positionalEncoding; // [maxLen, embeddingDim]
110+
// Host-side copy of the (constant) positional encoding. The forward assembles a per-window PE tensor from
111+
// it every step; indexing _positionalEncoding[i] on a GPU-resident tensor would sync per element (seqLen*dim
112+
// reads per forward). The PE never changes after construction, so cache one host download and read that.
113+
private T[]? _positionalEncodingHost;
110114

111115
// Encoder components
112116
private readonly List<AutoformerEncoderLayer<T>> _encoderLayers;
@@ -195,6 +199,7 @@ private void InitializeModel()
195199
// Sinusoidal positional encoding
196200
int maxLen = Math.Max(_options.LookbackWindow, _options.ForecastHorizon) * 2;
197201
_positionalEncoding = CreateSinusoidalPositionalEncoding(maxLen, _options.EmbeddingDim);
202+
_positionalEncodingHost = _positionalEncoding.GetCpuData();
198203

199204
// Encoder layers with series decomposition and auto-correlation
200205
for (int i = 0; i < _options.NumEncoderLayers; i++)
@@ -468,7 +473,8 @@ private Tensor<T> ForwardCore(Vector<T> input)
468473
var inputTensor = new Tensor<T>(new[] { seqLen, 1 }, inData);
469474

470475
var posData = new Vector<T>(seqLen * embDim);
471-
for (int i = 0; i < seqLen * embDim; i++) posData[i] = _positionalEncoding[i];
476+
var peHost = _positionalEncodingHost ??= _positionalEncoding.GetCpuData();
477+
for (int i = 0; i < seqLen * embDim; i++) posData[i] = peHost[i];
472478
var posTensor = new Tensor<T>(new[] { seqLen, embDim }, posData);
473479

474480
// Embedding: input @ inputProj(1×embDim) + positional encoding.
@@ -572,9 +578,13 @@ private Tensor<T> AutoCorrelationEngine(Tensor<T> q, Tensor<T> k, Tensor<T> v)
572578
var corr = Engine.Concat(corrParts, 0); // [corrLen]
573579

574580
// Top-k delays by correlation value (host read of the forward values; the index choice is
575-
// non-differentiable, like the paper's topk over the autocorrelation spectrum).
581+
// non-differentiable, like the paper's topk over the autocorrelation spectrum). Download the whole
582+
// [corrLen] vector in ONE device->host copy: indexing corr[lag] per element on a GPU-resident tensor
583+
// forces a separate transfer + full pipeline sync each time (corrLen syncs per call, ×4 calls per
584+
// forward, ×samples, ×epochs) — the dominant cost that left the GPU ~7% busy while the model timed out.
585+
var corrHost = corr.GetCpuData();
576586
var corrVals = new double[corrLen];
577-
for (int lag = 0; lag < corrLen; lag++) corrVals[lag] = _numOps.ToDouble(corr[lag]);
587+
for (int lag = 0; lag < corrLen; lag++) corrVals[lag] = _numOps.ToDouble(corrHost[lag]);
578588
var topLags = Enumerable.Range(0, corrLen)
579589
.OrderByDescending(i => corrVals[i])
580590
.Take(topK)
@@ -886,6 +896,7 @@ protected override void DeserializeCore(BinaryReader reader)
886896
// Read tensors
887897
_inputProjection = ReadTensor(reader);
888898
_positionalEncoding = ReadTensor(reader);
899+
_positionalEncodingHost = _positionalEncoding.GetCpuData();
889900
_decoderSeasonalInit = ReadTensor(reader);
890901
_decoderTrendInit = ReadTensor(reader);
891902
_seasonalProjection = ReadTensor(reader);

src/TimeSeries/InformerModel.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ public class InformerModel<T> : TimeSeriesModelBase<T>, ISupportsLossFunction<T>
7676
// Input embedding and positional encoding (Tensor-based)
7777
private Tensor<T> _inputProjection; // [embeddingDim, 1]
7878
private Tensor<T> _positionalEncoding; // [maxLen, embeddingDim]
79+
// Host-side copy of the (constant) positional encoding. The forward assembles the per-batch PE from it
80+
// every step; indexing _positionalEncoding[i] on a GPU-resident tensor syncs per element
81+
// (batch*seqLen*dim reads per forward). The PE never changes, so cache one host download.
82+
private T[]? _positionalEncodingHost;
7983

8084
// Encoder components with distilling (Tensor-based)
8185
private readonly List<InformerEncoderLayerTensor<T>> _encoderLayers;
@@ -170,6 +174,7 @@ private void InitializeModel()
170174
// Sinusoidal positional encoding for the maximum sequence length
171175
int maxLen = Math.Max(_options.LookbackWindow, _options.ForecastHorizon) * 2;
172176
_positionalEncoding = CreateSinusoidalPositionalEncoding(maxLen, _options.EmbeddingDim);
177+
_positionalEncodingHost = _positionalEncoding.GetCpuData();
173178

174179
// Encoder layers with distilling
175180
int currentSeqLen = _options.LookbackWindow;
@@ -727,10 +732,11 @@ private Tensor<T> ForwardBatch(Tensor<T> batchInput, int batch, int encLen0)
727732
var flatIn = Engine.Reshape(batchInput, new[] { batch * encLen0, 1 });
728733
var proj = Engine.TensorMatMul(flatIn, Engine.Reshape(_inputProjection, new[] { 1, d })); // [B*L, d]
729734
var posEnc = new Vector<T>(batch * encLen0 * d);
735+
var peHost = _positionalEncodingHost ??= _positionalEncoding.GetCpuData();
730736
for (int bi = 0; bi < batch; bi++)
731737
for (int t = 0; t < encLen0; t++)
732738
for (int j = 0; j < d; j++)
733-
posEnc[(bi * encLen0 + t) * d + j] = _positionalEncoding[t * d + j];
739+
posEnc[(bi * encLen0 + t) * d + j] = peHost[t * d + j];
734740
var embFlat = Engine.TensorAdd(proj, new Tensor<T>(new[] { batch * encLen0, d }, posEnc));
735741

736742
// Encoder stack with distilling.
@@ -751,7 +757,7 @@ private Tensor<T> ForwardBatch(Tensor<T> batchInput, int batch, int encLen0)
751757
var decPosData = new Vector<T>(horizon * d);
752758
for (int t = 0; t < horizon; t++)
753759
for (int j = 0; j < d; j++)
754-
decPosData[t * d + j] = _positionalEncoding[(_options.LookbackWindow + t) * d + j];
760+
decPosData[t * d + j] = peHost[(_options.LookbackWindow + t) * d + j];
755761
var decPos = new Tensor<T>(new[] { 1, horizon, d }, decPosData);
756762
var start3 = Engine.Reshape(_decoderStartToken, new[] { 1, 1, d });
757763
var dec3 = Engine.TensorBroadcastAdd(
@@ -972,6 +978,7 @@ protected override void DeserializeCore(BinaryReader reader)
972978
// Read main tensors
973979
_inputProjection = ReadTensor(reader);
974980
_positionalEncoding = ReadTensor(reader);
981+
_positionalEncodingHost = _positionalEncoding.GetCpuData();
975982
_decoderStartToken = ReadTensor(reader);
976983
_outputProjection = ReadTensor(reader);
977984
_outputBias = ReadTensor(reader);

src/TimeSeries/TemporalFusionTransformer.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ public class TemporalFusionTransformer<T> : TimeSeriesModelBase<T>
6060

6161
// Sinusoidal positional encoding [maxLen, hiddenSize] (replaces the LSTM scan).
6262
private Tensor<T> _positionalEncoding;
63+
// Host-side copy of the (constant) positional encoding. The forward assembles the per-batch PE from it
64+
// every step; indexing the tensor per element syncs the GPU batch*seq*dim times per forward.
65+
private T[]? _positionalEncodingHost;
6366

6467
// Variable selection (softmax-weighted feature combination).
6568
private GatedResidualNetwork<T> _vsnFeatureGrn; // processes embedded features
@@ -133,6 +136,7 @@ private void InitializeComponents()
133136

134137
// Positional encoding for the full lookback window.
135138
_positionalEncoding = CreateSinusoidalPositionalEncoding(Math.Max(1, _options.LookbackWindow), d);
139+
_positionalEncodingHost = _positionalEncoding.GetCpuData();
136140

137141
// Variable selection GRNs.
138142
_vsnFeatureGrn = new GatedResidualNetwork<T>(d, d, d, seed: _random.Next());
@@ -348,10 +352,11 @@ private Tensor<T> ForwardBatch(Tensor<T> batchInput, int batch, int seq)
348352
var emb = Engine.TensorMatMul(flatIn, _inputEmbeddingWeight); // [B*L, d]
349353
emb = Engine.TensorBroadcastAdd(emb, Engine.Reshape(_inputEmbeddingBias, [1, d]));
350354
var posData = new Vector<T>(batch * seq * d);
355+
var peHost = _positionalEncodingHost ??= _positionalEncoding.GetCpuData();
351356
for (int bi = 0; bi < batch; bi++)
352357
for (int t = 0; t < seq; t++)
353358
for (int j = 0; j < d; j++)
354-
posData[(bi * seq + t) * d + j] = _positionalEncoding[t * d + j];
359+
posData[(bi * seq + t) * d + j] = peHost[t * d + j];
355360
emb = Engine.TensorAdd(emb, new Tensor<T>([batch * seq, d], posData));
356361

357362
// 2. Variable selection: softmax-gated combination of processed features.

0 commit comments

Comments
 (0)