Autograd node for the single-layer LSTM with BPTT backward.
Implements the standard Hochreiter & Schmidhuber (1997) LSTM gating equations:
where is the logistic sigmoid, is the element-wise
(Hadamard) product, and the four gates are stored in
that exact concatenation order along the leading axis of
gates_all (i.e. gates_all[..., 0:H] is the input-gate
pre-activation, gates_all[..., H:2H] the forget gate, and so on).
Inherits directly from Node rather than FuncOp because the
7-edge topology {input, h0, c0, wih, whh, bih, bhh} is built by
hand: each tensor that requires a gradient gets an explicit
AccumulateGrad leaf wired in forward(), and non-differentiable
tensors (such as biases when has_bias=false) get a null edge.
Only the output sequence res[0] carries this node as its
grad_fn; hn and cn are detached so they can be reused as
initial states for a subsequent stacked layer in pure Python without
double-counting gradients.
Math
The BPTT backward unrolls time steps in reverse, accumulating the
gradient contributions through both the cell-state path
and the gate path
, then maps cell-state
gradients back through the sigmoid / tanh derivatives to give
per-time-step weight gradients ,
, ,
. The full unrolled BLAS path
lives in IBackend::lstm_backward.
References
Hochreiter & Schmidhuber, "Long Short-Term Memory" (Neural Computation 1997). Hochreiter, "Untersuchungen zu dynamischen neuronalen Netzen" (1991) for the original analysis of the vanishing-gradient problem that motivates the gating mechanism.
Attributes
saved_inputStorage(T, B, input_size) retained for the backward pass.saved_h0Storage(1, B, H_rec) where H_rec is proj_size if projection is enabled, otherwise hidden_size.saved_weightsstd::vector<Storage>{wih, whh, bih, bhh} in that exact order. When opts.has_bias == false the bias slots still appear but are zero-filled stand-ins so the backward kernel's BLAS layout is uniform.gates_allStorage(T, B, 4H) in [i, f, g, o] order, written by the training-mode forward.cells_allStoragec_0, c_1, ..., c_T shaped (T+1, B, H); cells_all[0] is the supplied c_0 and cells_all[T] is cn.forward() to apply() — input_size, hidden_size, seq_len, batch_size, has_bias, proj_size. num_layers and bidirectional are always 1 and false here; multi-layer and bidirectional configurations are composed in Python.dtypeDtypeapply(). Default: Dtype::F32.deviceDeviceNotes
Backend dispatch. CPU runs Apple Accelerate-backed hand-rolled
BLAS through lstm_forward_train / lstm_backward. GPU dispatches
to the MLX backend. The Python wrapper in
lucid.nn.modules.rnn.LSTM always invokes the engine
one-layer-one-direction at a time and composes stacking,
bidirectionality and inter-layer dropout itself, so this node only
ever sees num_layers == 1 and bidirectional == false.
Projection variant (lstm_proj). When opts.proj_size > 0
the recurrent path applies an additional learnable projection
to , so the
hidden state fed to the next step has dimension proj_size. The
cell state c_t is left at dimension hidden_size. This matches
the LSTMP variant used in many speech-recognition stacks and reduces
recurrent compute when proj_size < hidden_size.
Peephole connections are not supported — Lucid's LSTM follows the vanilla 1997 formulation without direct cell-to-gate links.
Static methods
1forward
→std::vector<TensorImplPtr>int forward(const int & input, const int & h0, const int & c0, const int & weights, const backend::IBackend::LstmOpts & opts)Run the LSTM forward pass and return {output, hn, cn}.
Decides between inference and training paths based on
GradMode::is_enabled() and whether any of input, h0,
c0 or the elements of weights require a gradient. On the
training path the backend's lstm_forward_train is invoked and
a fresh LstmBackward node is wired with saved_input,
saved_h0, saved_weights, gates_all and cells_all
populated for BPTT. On the inference path the backend's lighter
lstm_forward may be used (some backends route this through
lstm_forward_train and discard the saved tensors).
Parameters
inputTensorImplPtr(T, B, input_size). The batch_first flag in opts is forwarded but the Python LSTM wrapper always passes sequence-first.h0TensorImplPtr(1, B, H_rec) where H_rec == proj_size if projection is enabled, else hidden_size.c0TensorImplPtr(1, B, hidden_size).weightsstd::vector<TensorImplPtr>{W_ih, W_hh, b_ih, b_hh} (and a trailing W_hr when opts.proj_size > 0). Each gate weight is the vertically-stacked 4-gate matrix in [i, f, g, o] order — see the class docstring's Math section.Returns
std::vector<TensorImplPtr>Three tensors {output, hn, cn} where output is shaped (T, B, H_out), hn is shaped (1, B, H_out) and cn is shaped (1, B, hidden_size). Only output carries an autograd edge when the training path is taken; hn and cn are returned detached.
Raises
ErrorBuilderinput, h0, c0 or a weight tensor is null, or if a backend method is not implemented for the current device.Methods
2Run the BPTT backward and return per-edge gradients.
Dispatches to IBackend::lstm_backward which unrolls time
steps in reverse using the saved gates_all and cells_all
tensors. Gradients for hn and cn at the sequence end are
synthesised as zero buffers (matching detached outputs), because
only the output sequence res[0] is connected in the autograd
graph for the single-layer case.
Parameters
grad_outStorage(T, B, H_out) where H_out == proj_size if projection is enabled, else hidden_size.Returns
std::vector<Storage>Seven Storage objects in edge order {dInput, dh0, dc0, dW_ih, dW_hh, db_ih, db_hh}. Bias slots are zero buffers when opts.has_bias == false.
Return the node name for debug / graph visualisation.
Returns
std::string_viewThe literal "LstmBackward".