LSTMwith its shapes on

III · Forward

The forward pass, with its shapes

One modern, no-peephole LSTM layer. This is our batched pedagogical presentation, not a verbatim reproduction of the 1997 or 2013 papers. The translation page explains their architectural differences.

Source family: Graves 2013, §2.1, Eqs. 7–11, with all peepholes removed and the cell candidate named g. Fusing gates, batching and row-oriented storage are presentation/implementation choices. Sources and exact adaptations.

Axis identityMeaningSize
independent sequences in a batch16
time steps128
input embedding coordinates512
recurrent state coordinates (source of the recurrent projection)768
projection output coordinates, distinct from D; canonical coordinate-wise bijection R → D768
gate labels, in order i, f, g, o4
fused gate-output axis; an atomic identity until unfused3072 = 4·768
input and recurrent state concatenated, in that order1280

Sizes are not axis identities. R and D both have 768 entries but cannot be silently matched. K has 3072 entries (often called “4D” in code); it does not share a free D or R axis with another tensor. A size-only numerical view is a check against this ledger, never a replacement for its identities.

Parameters

W has 1,572,864 entries; U has 2,359,296; the stored bias has 3072. K is a fused destination axis, not the source D. Unfused slices of U have D×R, so the two hidden directions cannot collide by name.

Here D and E in counts denote cardinalities. PyTorch stores two additive bias vectors: its one-layer parameter count is 4D(E+D+2)=3,938,304. Its stored weights have K×E and K×D layout, the reverse of the layout used here; matching named axes does not depend on their order. See the nn.LSTM equations and variables.

Read one slice of the input

X contains 16×128×512 = 1,048,576 entries. Each time slice is 16×512. Start with .

Project the input and the previous state

In numbers: (16×512) times (512×3072), and (16×768) times (768×3072). Both yield 16×3072. The source E or D is summed; K survives. Only the second depends on the previous step.

The input projection can be computed across all times at once; this is an algebraic possibility, not a claim that every backend always does so.

Add the explicitly broadcast bias

The same 3072 bias numbers are added to each batch row. The equivalent concatenated calculation is:

J=E+D=1280. This is (16×1280) times (1280×3072). Concatenation chooses the input-first ordering explicitly.

Unpack and rename the gate coordinates

Unpack means: unfuse K into G×R, select each gate (i,f,g,o), then apply the coordinate-wise bijection ρ from R to D. Before the rename, every gate slice is B×R. Afterwards it is B×D. This explicit map is required even though |R|=|D|=768; no D×D dense map is hiding here.

Apply the element-wise nonlinearities

All four have 16×768 entries. i, f and o are in (0,1); g is in (−1,1).

Update the cell and read it out

There is an additive, coordinate-wise route from c at one step to the next. That route can help preserve gradients when forget gates stay near one. It does not prevent vanishing gradients: repeated forget gates below one attenuate it. Gates also depend on h, which introduces other recurrent paths. The backward page distinguishes these derivatives precisely.

Repeat and stack

The output is 16×128×768 = 1,572,864 entries (6.29 MB in fp32). The two matrix products cost about 2BK(E+D)=125,829,120 FLOPs per batch step, or 16.1 GFLOPs over the whole batch sequence. This ignores nonlinearities and element-wise work.

The short form

All gate and preactivation symbols are declared as B×D families above. E1–E5 allow:

A = X @ W                              # B,T,K; K = 4*D
for t in range(T):
    z = A[:, t] + h @ U + b             # B,K; virtual B bias axis
    qi, qf, qg, qo = z.split(D, -1)     # K -> G,R; views R renamed D
    i, f, g, o = qi.sigmoid(), qf.sigmoid(), qg.tanh(), qo.sigmoid()
    c = f * c + i * g                   # B,D
    h = o * c.tanh()                    # B,D
    hs.append(h)
H = torch.stack(hs, dim=1)              # B,T,D