2 Deep Learning

2.1 Linear Classification

Given a dataset \( \mathbf {D} = \{(x_i, y_i)\}_{i=1}^n, x_i \in \mathbb {R}^d,\; y_i \in \{\pm 1\},\) a classifier is a function with adjustable parameters \(\theta \), \( \mathbf {f}_{\theta }: \mathbb {R}^d \rightarrow \{\pm 1\}\), that predicts the output label conditioned on \(x_i\). For example, inferring whether the behavior of a given program is malicious is a classification problem, where the label could be \(-1\) for malicious and \(1\) for benign. The goal is to have \(\mathbf {f}_{\theta }(x_i) \approx y_i\).

A linear classifier has a score of the form

\[ \mathbf {s}_{\theta }(x) = w^T x + b \]

Where \(\theta = \{ w,b\}\), \(b\) is the bias that shifts the decision boundary and \(w\) determines the orientation of the decision boundary (given by \(w^T x + b = 0\) ). If \(w^T x + b \geq 0\) then the predicted label is 1 otherwise -1. The classifier is then defined as

\[ \mathbf {f}_{\theta }(x) = \operatorname {sign}(s_{\theta }(x)) = \operatorname {sign}(w^T x + b) = \operatorname {sign}(\tilde {w}^T \tilde {x}) \]

where

\[ \tilde {w} = \begin {pmatrix} w \\ b \end {pmatrix}, \qquad \tilde {x} = \begin {pmatrix} x \\ 1 \end {pmatrix} \]

If a linear classifier classifies the data correctly, then the data is said to be linearly separable. More formally, a dataset is linearly separable if there exist \(w\) and \(b\), i.e., a hyperplane, such that

\[ y_i(w^T x_i + b) > 0 \qquad \forall i \]

The minimum margin, i.e distance to closest sample, is given by:

\[ \min _i y_i \frac {w^T x_i + b}{\|w\|}. \]

Where we divide by \(||w||\) otherwise the projection is scaled by the size of \(w\). We multiply by \(y_i\) so that the margin is always positive for correctly classified samples, regardless of which side of the hyperplane they lie (one can also just use \(\frac {|w^T x_i + b|}{\|w\|} )\). The signed distance to the hyperplane is \(\frac {w^T x_i + b}{\|w\|}\); a negative value indicates that the sample lies on the negative side of the hyperplane. One algorithm for finding such a separating hyperplane is the Perceptron Algorithm or Gradient Ascent.

PIC

One can formulate the search for the maximum margin separating hyperplane as the following constrained optimization problem:

\[ \max _{w,b} \left \{ \min _i y_i \frac {w^T x_i+b}{\|w\|} \right \} \qquad \text {such to} \qquad y_i(w^T x_i+b)>0, \quad \forall i \]

Given the produced score \(w^T \mathbf {x} + b\), one can turn it into a proper probability distribution using the sigmoid function:

\[ p_{\theta }(y = 1 \mid \mathbf {x}) = \frac {e^{w^T \mathbf {x}+b}} {1 + e^{w^T \mathbf {x}+b}} = \frac {1} {1 + e^{-(w^T \mathbf {x}+b)}}. \]

which parameterizes the Bernoulli distribution with probability mass \(p_{\theta }(\mathbf {y} \mid \mathbf {x}) = (p_{\theta }(y = 1 \mid \mathbf {x}))^{\frac {1+y}{2}} (1-p_{\theta }(y = 1 \mid \mathbf {x}))^{\frac {1-y}{2}}\). This makes sense because \(p_{\theta }(y = 1 \mid \mathbf {x}) > \frac {1}{2}\) if and only if \(w^Tx + b> 0\).

Now, consider the scenario where we have \(L\) labels, then the classifier produces one score per class, we could write the output as \(s_{\theta }(x) = Wx + b \in \mathbb {R}^{L}\), where \(s_{\theta }(x)\) is commonly referred to as the logits. Here, the input is projected onto \(L\) different weight vectors, each with its own bias and the predicted class corresponds to the index with the highest score i.e \(\arg \max _k (Wx+b)_k\). The classifier is defined as \(\mathbf {f}_{\theta }(x) = \arg \max _{k \in \{1,...,L\}} (Wx + b)_k\). One way to learn the parameters \(W \in \mathbb {R}^{L \times d}\) and \(b \in \mathbb {R}^{L}\) is to use the Multiclass Perceptron algorithm or Gradient Ascent .

Similar to the binary case, one can turn the score function into a proper conditional distribution by using the softmax operation \( p_{\theta }(y=k \mid x) = \operatorname {softmax}(s_{\theta }(x))_k \). It makes sense because the softmax preserves ordering. The probability vector \(\pi = (\pi _1,...,\pi _L)\) where \(\pi _k:= p_{\theta }(y=k \mid x)\) and \(\sum _{k=1}^{L} \pi _k = 1\) parametrizes a categorical distribution \(\text {Cat}(y, \pi = \operatorname {softmax}(Wx+b))\) with the following probability mass function:

\[ p_{\theta }(y \mid x) = \prod _{k=1}^{L} \pi _k^{\mathbf {1}_{y=k}} \]

Now remember, we learned that we can interpret the output of a model as the parameters of a probability distribution. Later, we will see how deep neural networks are trained to learn these parameters so that the resulting conditional distribution approximates the empirical conditional distribution using maximum likelihood estimation. Parametrizing a distribution \(p(\mathbf {x})\) with neural networks provides more flexibility than, for instance, parametrizing the distribution with a linear layer. However, this increased flexibility also comes at a cost, as the resulting models can be more difficult to train, and some quantities of interest (e.g., likelihood or data density) may no longer be tractable in closed form (e.g., in a VAE), requiring us, for instance, to optimize a lower bound instead (we will discuss this later). Additionally, using neural networks to parametrize densities can introduce an identifiability problem. Basically, two learned models with different parameters or weights can produce two manifolds that are identical, and thus we can parametrize the same manifold in different ways. This is known as a reparametrization issue or an identifiability problem [1, 2, 3]. In this case, plotting and inspecting the latent space may not be a good idea for analysis. An example of an identifiable distribution is the Gaussian distribution, as changing its parameters (its mean and variance) results in a different statistical model. Probabilistic PCA [4] and densities parametrized by neural networks are generally not identifiable (e.g., a VAE).

Linear classification is often insufficient because its decision boundary is restricted to a hyperplane in the original input space. In many practical problems, the target classes are not linearly separable in this space. Several nonlinear transformations may therefore be required to map the input to a representation in which the classes become linearly separable. This is one of the motivations behind deep neural networks.

2.2 Deep Learning

Deep learning consists of learning a sequence of transformations stacked together, where each layer receives the output of the previous layer, applies a linear transformation followed by a nonlinear activation function, and produces an output for the next layer. This is why it is called deep, as many layers are stacked together, and learning, because the parameters of these transformations, typically projection matrices for fully connected layers or kernels for convolutional layers, are learned from the dataset.

Binary Classification

In the field of machine and deep learning, we assume that the dataset \( \mathbb {D} = \{(x_i,y_i)\mid x_i\in \mathbb {X},\,y_i\in \mathbb {Y}\}, \) was generated through a random sampling process following unknown data distribution \(p_{\mathrm {data}}(x,y)\), that is, \((x_i,y_i)\sim p_{\mathrm {data}}(x,y)\). We do not get to know this distribution. However, we can approximate it using the empirical distribution.

The empirical joint distribution is given by

\[ \hat {p}_{\text {data}}(x,y) = \frac {1}{N} \sum _{i=1}^{N} \delta (x-x_i)\,\delta (y-y_i) \]

while the empirical marginal distribution of \(x\) is

\[ \hat {p}_{\text {data}}(x) = \frac {1}{N} \sum _{i=1}^{N} \delta (x-x_i) \]

Recall that in binary classification, the goal is to learn a mapping from the input space \(\mathbb {X}\) to the label space \(\mathbb {Y}\), \( f_\theta (x)\in \mathbb {Y} =\{0,1\} \).

PIC

Instead of learning to map to the label directly, we will instead learn to predict, i.e interpret \(f_\theta (x)\) as \( p_\theta (y=1\mid x)\). The output of the model \(f_\theta (x)\) then parameterizes the Bernoulli distribution \(p_\theta (y\mid x)\):

\[ p_\theta (y\mid x) = p_\theta (y=1\mid x)^y \left (1-p_\theta (y=1\mid x)\right )^{1-y} \]

or if you will

\[ p_\theta (y\mid x) = f_\theta (x)^y \left (1-f_\theta (x)\right )^{1-y}. \]

We want to find \(\theta ^{*} := \arg \min _\theta \mathbb {E}_{(x,y)\sim p_{\mathrm {\text {data}}}(x,y)} \left ( -\log p_\theta (y\mid x) \right ) = \arg \min _\theta H(p_{\text {data}}(y|x),p_{\theta }(y|x)\mathbf | p_\text {data}(x)) \)

That is, the objective function of supervised learning is to estimate the conditional (cross) entropy which measure distance between two densities. In fact, mimnimizing the cross entropy is the same as minimizing the KL divergence, which measures the discrepancy between the true conditional distribution and the model conditional distribution \(p_{\theta }(y|x)\) (as we can drop the parameter independent term), that is, \(\arg \min _\theta H(p_{\text {data}}(y|x),p_{\theta }(y|x)\mathbf | p_\text {data}(x)) = \arg \min _\theta D_{KL}(p_{data}(y|x),p_{\theta }(y|x)\mathbf | p_{\text {data}}(x) )\).

However, we do not know \(p_{\mathrm {data}}(x,y)\), so we will approximate it using the empirical distribution, that is, minimizing the negative conditional log-likelihood of the dataset \(\mathbb {D}\) (empirical cross entropy):

\[ \theta _{\mathrm {MLE}} := \arg \min _\theta \underbrace {\frac {1}{N} \sum _{i=1}^{N} -\log p_\theta (y_i\mid x_i)}_{\mathbf {L}_{\hat {p}_{\text {data}}}(\theta )} \]

By the weak law of large numbers \(\mathbf {L}_{\hat {p}_{\text {data}}}(\theta )\) converges to \(\mathbb {E}_{(x,y)\sim p_{\mathrm {\text {data}}}(x,y)} \left ( -\log p_\theta (y\mid x) \right )\) and thus we can justify using the empirical negative conditional log-likelihood to estimate \(\theta ^{*}\).

Using the Bernoulli probability mass function definition,

\[ \theta _{\mathrm {MLE}} = \arg \min _\theta \frac {1}{N} \sum _{i=1}^{N} - \left ( y_i\log f_\theta (x_i) + (1-y_i)\log \left (1-f_\theta (x_i)\right ) \right ) \]

We can define the loss (which tell us how bad the model is at prediction) as

\[ \mathbf {L}_{\hat {p}_{\text {data}}}(\theta ) = \frac {1}{N} \sum _{i=1}^{N} \underbrace { - \left [ y_i\log f_\theta (x_i) + (1-y_i)\log \left (1-f_\theta (x_i)\right ) \right ] }_{\mathcal {\mathbf l}(f_\theta (x_i),y_i)}. \]

The goal is then

\[ \min _\theta \mathbf {L}_{\hat {p}_{\text {data}}}(\theta ) \]

where

\[ \mathbf {L}_{\hat {p}_{\text {data}}}(\theta ) \rightarrow _{n\rightarrow +\infty } \underbrace {\mathbb {E}_{(x,y)\sim p_{\mathrm {data}}} \left [ \mathcal {\mathbf l}(f_\theta (x),y) \right ]}_{\mathbf {L}_{p_{\text {data}}}(\theta )} \text {(in probability)} \]

Multiclass Classification

For multiclass classification, we parameterize a categorical distribution \(p_\theta (y\mid x)\).

The model produces a vector of scores followed by a softmax, resulting in

\[ f_\theta (x)\in \mathbb {R}^{K} \]

where \(K\) is the number of classes and

\[ (f_\theta (x))_k = p_\theta (y=k\mid x) \]

PIC

The categorical distribution can be written as

\[ p_\theta (y\mid x) = \prod _{k=1}^{K} (f_\theta (x))_k^{\mathbf {1}_{\{y=k\}}}, \qquad y\in \{1,\ldots ,K\} \]

We want to find

\[ \theta ^* := \arg \min _\theta \mathbb {E}_{(x,y)\sim p_{\mathrm {data}}(x,y)} \left ( -\log p_\theta (y\mid x) \right ) \]

As in the binary case, this corresponds to minimizing the conditional cross-entropy

\[ \theta ^* = \arg \min _\theta H\left ( p_{\mathrm {data}}(y\mid x), p_\theta (y\mid x) \mid p_{\mathrm {data}}(x) \right ) \]

Since

\[ \begin {aligned} &H\left ( p_{\mathrm {data}}(y\mid x), p_\theta (y\mid x) \mathbf { | } p_{\mathrm {data}}(x) \right ) \\ &\qquad = H\left ( p_{\mathrm {data}}(y\mid x) \mathbf { | } p_{\mathrm {data}}(x) \right ) + D_{\mathrm {KL}}\left ( p_{\mathrm {data}}(y\mid x) \| p_\theta (y\mid x) \mathbf { | } p_{\mathrm {data}}(x) \right ), \end {aligned} \]

and the first term does not depend on \(\theta \), minimizing the conditional cross-entropy is equivalent to minimizing the conditional KL divergence. However, again we do not know \(p_{\mathrm {data}}(x,y)\), so we approximate it using the empirical distribution. We obtain the MLE

\[ \theta _{\mathrm {MLE}} = \arg \min _\theta \frac {1}{N} \sum _{i=1}^{N} -\log p_\theta (y_i\mid x_i) \]

Using the categorical probability mass function,

\[ \theta _{\mathrm {MLE}} = \arg \min _\theta \frac {1}{N} \sum _{i=1}^{N} \left ( -\sum _{k=1}^{K} \mathbf {1}_{\{y_i=k\}} \log (f_\theta (x_i))_k \right ) \]

For each sample, the inner term can also be written as

\[ -\sum _{k=1}^{K} \mathbf {1}_{\{y_i=k\}} \log (f_\theta (x_i))_k = -\operatorname {onehot}(y_i)^T \log f_\theta (x_i) \]

The loss can then be formulated as

\[ \mathbf {L}_{\hat {p}_{\text {data}}}(\theta ) = \frac {1}{N} \sum _{i=1}^{N} \underbrace { -\operatorname {onehot}(y_i)^T \log f_\theta (x_i) }_{\mathbf l(f_\theta (x_i),y_i)}. \]

For the empirical conditional distribution,

\[ \hat {p}_{\text {data}}(y=k\mid x_i) = \mathbf {1}_{\{y_i=k\}} \]

and therefore we can equivalently write

\[ \theta _{\mathrm {MLE}} = \arg \min _\theta \frac {1}{N} \sum _{i=1}^{N} \left ( -\sum _{k=1}^{K} \hat {p}_{\text {data}}(y=k\mid x_i) \log p_\theta (y=k\mid x_i) \right ) \]

By the weak law of large numbers, for a fixed \(\theta \),

\[ \mathbf {L}_{\hat {p}_{\text {data}}}(\theta ) \xrightarrow [N\to \infty ]{P} \mathbb {E}_{(x,y)\sim p_{\mathrm {data}}(x,y)} \left ( \mathbf l (f_\theta (x),y) \right ) \]

2.3 Discriminative and Generative Modeling

Deep learning models can broadly be divided into two categories: discriminative models and generative models. Discriminative models learn the conditional distribution \(p_\theta (y \mid x)\), where \(x\) denotes the input and \(y\) the corresponding target. The objective is to approximate the unknown data distribution \(p_{\text {data}}(y \mid x)\). For example, in binary classification, the output of the neural network is interpreted as the parameter of a Bernoulli distribution, as covered earlier, \(p_\theta (y=1\mid x)=f_\theta (x)\) where \(f_\theta (\cdot )\) denotes the deep neural network. Given training samples \(\{(x_i,y_i)\}\) assumed to be sampled from the unknown distribution \(p_{\text {data}}(x,y)\), the model is typically trained by minimizing the cross-entropy loss, which is equivalent to maximizing the likelihood of the observed labels or, equivalently, minimizing the KL divergence between the true conditional distribution \(p_{\text {data}}(y\mid x)\) and the model distribution \(p_\theta (y\mid x)\). The empirical cross-entropy converges in probability to the conditional cross-entropy as the number of samples increases (\(n\rightarrow +\infty \)).

Generative models, on the other hand, learn how the data itself is generated by approximating the data distribution \( p_{\text {data}}(x) \). For example, Generative Adversarial Networks or GANs learn an implicit distribution by mapping samples drawn from a simple prior distribution \(p(z)\), typically a standard Gaussian, through a generator network \(G_\theta (z)\). On the other hand, Variational Autoencoders or VAEs explicitly introduce a latent variable \(z\) and model the joint distribution \( p_\theta (x,z)=p_\theta (x\mid z)p(z)\), where \(p(z)\) is a predefined prior, usually a standard Gaussian. The data likelihood is then given by \( p_\theta (x) = \int p_\theta (x\mid z)p(z)\,dz, \) which is generally intractable to compute exactly. Large Language Models or LLMs are also generative models. They learn the joint probability distribution of a sequence of tokens by factorizing it using the chain rule (autoregressive or AR modeling), \( p_{\theta }(x_1,\ldots ,x_n) = p_{\theta }(x_1) \prod _{i=2}^{n} p_{\theta }(x_i\mid x_1,\ldots ,x_{i-1}). \). Training consists of learning each conditional distribution so that the model can accurately predict the next token given all previous ones (which is easier than generating celebrity faces). Once trained, the model can either compute the likelihood of a sequence or generate new text by repeatedly sampling or selecting the next token from the learned conditional distribution.

2.4 The Variational Autoencoder (VAE) framework

In the field of deep learning, we assume that the data was generated from some unknown distribution (i.e there is a data-generating process behind), and we attempt to approximate it using a parametric model \(p_\theta (x)\). We observe random samples \(\{x_i\}_{i=1}^n\) where \(x_i \sim p_{\text {data}}(x)\) (the datapoints are assumed to be iid). The goal of learning is to find \(\theta \) such that \(p_\theta (x) \approx p_{\text {data}}(x)\) i.e., we approximate the underlying data distribution (using the empirical distribution). Once we have learned \(p_\theta (x)\), we can generate artificial or synthetic samples. However, under the manifold hypothesis, \(p_{\text {data}}(x)\) is concentrated on a low-dimensional manifold embedded in a very high-dimensional ambient space. This motivates introducing a lower-dimensional latent space from which the observed data can be generated.

PIC

We learn how to transform a simple fixed distribution in latent space, for example \(p(z)=\text {Normal}(0,I)\), into the data distribution. Rather than learning a mapping as in GANs, we use conditional modeling and learn the conditional density \(p_\theta (x \mid z)\). More precisely, we model

\[ p_\theta (x \mid z) \approx p_{\text {data}}(x \mid z) \]

To estimate \(p_{\text {data}}(x \mid z)\), we use a deep neural network whose output parameterizes the conditional distribution \(p_\theta (x \mid z)\) (i.e., the decoder parameterizes this distribution). We assume that the observed data were generated from latent variables, i.e., pairs \((x,z)\) were generated, but only \(x\) is observed while \(z\) remains hidden.

Since \(x\) and \(z\) are dependent, we can write the marginal likelihood as (assuming the prior is fixed and known)

\[ p_\theta (x) = \int p_\theta (x,z)\,dz = \int p_\theta (x\mid z)p_{\theta }(z)\,dz = \mathbb {E}_{p_{\theta }(z)}\!\left [p_\theta (x\mid z)\right ] \]

Our objective is to learn \(\theta \) such that

\[ p_\theta (x) \approx p_{\text {data}}(x) \]

Unfortunately, \(p_\theta (x)\) intractable (i.e no analytic solution) because \(p_\theta (x\mid z) = \mathcal {N}\!\left (x;f_\theta (z),\sigma ^2 I\right )\) is parameterised using a multi-layer neural-network decoder, making the integral impossible to evaluate analytically. We could use naive Monte Carlo estimation by sampling a large number of latent variables from the prior \(z_k \sim p(z)\) and compute \(p_\theta (x)\) approximately

\[ p_\theta (x) \approx \frac {1}{K}\sum _{k=1}^{K} p_\theta (x\mid z_k), \]

Giving

\[ \log p_\theta (x) \approx \log \left ( \frac {1}{K} \sum _{k=1}^{K} p_\theta (x\mid z_k) \right ) \]

While writing the marginal likelihood as an expected value with respect to the prior distribution seems to solve the problem, it leads to an unstable estimator, as most of the sampled \(z\) values will result in likelihood values close to zero. We are likely to miss, by chance, those samples that would result in high likelihood values, thereby increasing the variance of the estimator (very large random samples are needed) [5, 6]. To see why, imagine drawing latent vectors from the prior. For a given observation \(x\), only a small subset of latent variables \(z\) are likely to explain it well. Consequently,\( p_\theta (x\mid z)\approx 0\) for most sampled \(z\). By Bayes’ rule,

\[ p_\theta (z\mid x) = \frac {p_\theta (x\mid z)p(z)}{p_\theta (x)} \]

this implies that the posterior places almost all of its probability mass on a very small region of the latent space, while assigning nearly zero probability to the rest.

The key idea behind Variational Autoencoder or VAE is therefore to introduce an approximate posterior distribution \(q_\phi (z\mid x)\) to produce latent variables that are likely to assign a high probability to the observation through \(p_\theta (x\mid z)\). This provides an efficient way to identify the regions of latent space that explain the data and serves as the first step toward learning a latent representation that can later be used for generation by sampling from the prior.

PIC

We start from the marginal likelihood

\[ p_\theta (x) = \frac {p_\theta (z,x)}{p_\theta (z\mid x)} \]

and therefore

\[ \log p_\theta (x) = \log p_\theta (z,x) - \log p_\theta (z\mid x) \]

Introducing an arbitrary distribution \(q(z)\), we can write

\[ \log p_\theta (x) = \log p_\theta (z,x) - \log q(z) + \log q(z) - \log p_\theta (z\mid x) \]

or,

\[ \log p_\theta (x) = \log \frac {p_\theta (z,x)}{q(z)} - \log \frac {p_\theta (z\mid x)}{q(z)} \]

Taking the expectation with respect to \(q(z)\),

\[ \mathbb {E}_{q(z)} \left [ \log p_\theta (x) \right ] = \mathbb {E}_{q(z)} \left [ \log \frac {p_\theta (z,x)}{q(z)} \right ] - \mathbb {E}_{q(z)} \left [ \log \frac {p_\theta (z\mid x)}{q(z)} \right ] \]

Since \(\log p_\theta (x)\) does not depend on \(z\),

\[ \log p_\theta (x) = \mathbb {E}_{q(z)} \left [ \log \frac {p_\theta (z,x)}{q(z)} \right ] + D_{\mathrm {KL}} \left ( q(z)\,\|\,p_\theta (z\mid x) \right ) \]

Using

\[ p_\theta (z,x) = p_\theta (x\mid z)p_\theta (z) \]

we obtain

\[ \log p_\theta (x) = \mathbb {E}_{q(z)} \left [ \log \frac { p_\theta (x\mid z)p_\theta (z) }{ q(z) } \right ] + D_{\mathrm {KL}} \left ( q(z)\,\|\,p_\theta (z\mid x) \right ) \]

Expanding the first term gives

\[ \log p_\theta (x) = \mathbb {E}_{q(z)} \left [ \log p_\theta (x\mid z) \right ] + \mathbb {E}_{q(z)} \left [ \log \frac {p_\theta (z)}{q(z)} \right ] + D_{\mathrm {KL}} \left ( q(z)\,\|\,p_\theta (z\mid x) \right ) \]

Since

\[ \mathbb {E}_{q(z)} \left [ \log \frac {p_\theta (z)}{q(z)} \right ] = - D_{\mathrm {KL}} \left ( q(z)\,\|\,p_\theta (z) \right ) \]

we obtain

\[ \log p_\theta (x) = \mathbb {E}_{q(z)} \left [ \log p_\theta (x\mid z) \right ] - D_{\mathrm {KL}} \left ( q(z)\,\|\,p_\theta (z) \right ) + D_{\mathrm {KL}} \left ( q(z)\,\|\,p_\theta (z\mid x) \right ) \]

Therefore,

\[ \log p_\theta (x) \geq \mathbb {E}_{q(z)} \left [ \log p_\theta (x\mid z) \right ] - D_{\mathrm {KL}} \left ( q(z)\,\|\,p_\theta (z) \right ) \]

because

\[ D_{\mathrm {KL}} \left ( q(z)\,\|\,p_\theta (z\mid x) \right ) \geq 0 \]

The term

\[ \mathbf {L}(\theta ,q;x) = \mathbb {E}_{q(z)} \left [ \log p_\theta (x\mid z) \right ] - D_{\mathrm {KL}} \left ( q(z)\,\|\,p_\theta (z) \right ) \]

is known as the Evidence Lower Bound or ELBO for short.

We now set

\[ q(z) = q_\phi (z\mid x) \]

where \(q_\phi (z\mid x)\) is parameterized by an encoder with parameters \(\phi \).

The VAE objective becomes

\[ \mathbf {L}_{\mathrm {VAE}}(\theta ,\phi ) = \mathbb {E}_{x\sim p_{\mathrm {data}}(x)} \left [ \mathbb {E}_{z\sim q_\phi (z\mid x)} \left [ \log p_\theta (x\mid z) \right ] - D_{\mathrm {KL}} \left ( q_\phi (z\mid x) \,\|\, p_\theta (z) \right ) \right ] \]

For the empirical data distribution,

\[ \mathbf {L}_{\mathrm {VAE}}(\theta ,\phi ) = \frac {1}{N} \sum _{i=1}^{N} \left [ \mathbb {E}_{z\sim q_\phi (z\mid x_i)} \left [ \log p_\theta (x_i\mid z) \right ] - D_{\mathrm {KL}} \left ( q_\phi (z\mid x_i) \,\|\, p_\theta (z) \right ) \right ] \]

where the expectation over \(q_\phi (z\mid x_i)\) can be approximated using Monte Carlo sampling.

We assume that \(q_\phi (z\mid x)\) parameterizes a Gaussian distribution.

For a latent space of dimension \(b\),

\[ q_\phi (z\mid x) = q_\phi (z_1, ..., z_b \mid x) = \prod _{k=1}^{b}q_\phi (z_k \mid x) = \prod _{k=1}^{b} \mathcal {N} \left ( z_k; \mu _k(x,\phi ), \sigma _k^2(x,\phi ) \right ) \]

Equivalently,

\[ q_\phi (z\mid x) = \mathcal {N} \left ( z; \mu _\phi (x), \operatorname {diag} \left ( \sigma _\phi ^2(x) \right ) \right ) \]

The encoder therefore outputs

\[ \mu _\phi (x) \qquad \text {and}\qquad \sigma _\phi (x) \]

or, in practice, typically

\[ \mu _\phi (x) \qquad \text {and}\qquad \log \sigma _\phi ^2(x) \]

Assume the prior is

\[ p_\theta (z) = \mathcal {N}(0,I) \]

Then the KL divergence has a closed-form expression:

\[ D_{\mathrm {KL}} \left ( q_\phi (z\mid x) \,\|\, p_\theta (z) \right ) = \frac {1}{2} \sum _{k=1}^{b} \left ( \mu _k^2(x,\phi ) + \sigma _k^2(x,\phi ) - 1 - \log \sigma _k^2(x,\phi ) \right ) \]

Therefore,

\[ - D_{\mathrm {KL}} \left ( q_\phi (z\mid x) \,\|\, p_\theta (z) \right ) = \frac {1}{2} \sum _{k=1}^{b} \left ( 1 + \log \sigma _k^2(x,\phi ) - \mu _k^2(x,\phi ) - \sigma _k^2(x,\phi ) \right ) \]

Hence, the loss can be simplified to

\[ \mathbf {L}_{\mathrm {VAE}}(\theta ,\phi ) = \frac {1}{N} \sum _{i=1}^{N} \left [ \mathbb {E}_{z\sim q_\phi (z\mid x_i)} \left [ \log p_\theta (x_i\mid z) \right ] + \frac {1}{2} \sum _{k=1}^{b} \left ( 1 + \log \sigma _k^2(x_i,\phi ) - \mu _k^2(x_i,\phi ) - \sigma _k^2(x_i,\phi ) \right ) \right ] \]

Now we need to to compute \( \frac {\partial \mathbf {L}}{\partial \theta } \qquad \text {and}\qquad \frac {\partial \mathbf {L}}{\partial \phi }\). Computing the gradient with respect to \(\theta \) is simple because we can use Monte Carlo sampling to eliminate expectation \(\mathbb {E}_{q_\phi (z\mid x_i)}\).

For example,

\[ \frac {\partial \mathbf {L}}{\partial \theta } \approx \frac {1}{N} \sum _{i=1}^{N} \frac {\partial }{\partial \theta } \log p_\theta (x_i\mid z_i), \qquad z_i\sim q_\phi (z\mid x_i) \]

But what about \( \frac {\partial \mathbf {L}}{\partial \phi }\)

This is a little problematic because the distribution from which we sample depends on \(\phi \).

In general,

\[ \mathbb {E}_{q_\phi (z\mid x_i)} \left [ \frac {\partial }{\partial \phi } \log p_\theta (x_i\mid z) \right ] \neq \frac {\partial }{\partial \phi } \mathbb {E}_{q_\phi (z\mid x_i)} \left [ \log p_\theta (x_i\mid z) \right ]. \]

For the second term,

\[ D_{\mathrm {KL}} \left ( q_\phi (z\mid x) \,\|\, p_\theta (z) \right ) \]

the closed-form expression depends explicitly on \(\phi \), and its gradient can be obtained using automatic differentiation.

The remaining problem is therefore how to compute

\[ \frac {\partial }{\partial \phi } \mathbb {E}_{q_\phi (z\mid x)} \left [ \log p_\theta (x\mid z) \right ] \]

Instead, we use the reparameterization trick.

We write

\[ z = \mu _\phi (x) + \sigma _\phi (x)\odot \epsilon , \qquad \epsilon \sim \mathcal {N}(0,I) \]

We can therefore write

\[ z = g_\phi (x,\epsilon ) = \mu _\phi (x) + \sigma _\phi (x)\odot \epsilon \]

Then

\[ \mathbb {E}_{q_\phi (z\mid x)} \left [ f(z) \right ] = \mathbb {E}_{p(\epsilon )} \left [ f\left (g_\phi (x,\epsilon )\right ) \right ] \]

The randomness has now moved from \(z\) to \(\epsilon \), whose distribution \( p(\epsilon )=\mathcal {N}(0,I)\) does not depend on \(\phi \). Consequently,

\[ \frac {\partial }{\partial \phi } \mathbb {E}_{q_\phi (z\mid x)} \left [ \log p_\theta (x\mid z) \right ] = \frac {\partial }{\partial \phi } \mathbb {E}_{p(\epsilon )} \left [ \log p_\theta \left ( x\mid g_\phi (x,\epsilon ) \right ) \right ] \]

Since \(p(\epsilon )\) does not depend on \(\phi \), we can move the derivative inside the expectation:

\[ \frac {\partial }{\partial \phi } \mathbb {E}_{q_\phi (z\mid x)} \left [ \log p_\theta (x\mid z) \right ] = \mathbb {E}_{p(\epsilon )} \left [ \frac {\partial }{\partial \phi } \log p_\theta \left ( x\mid \mu _\phi (x) + \sigma _\phi (x)\odot \epsilon \right ) \right ] \]

We can now use Monte Carlo sampling. Observe that \(p_\theta (x\mid z)\) depends on \(\phi \) through \( z = \mu _\phi (x) + \sigma _\phi (x)\odot \epsilon \). For example, if the decoder distribution \(p_\theta (x\mid z)\) is Gaussian, the reconstruction term can correspond to an \(L_2\) loss.

We can therefore write the VAE objective as

\[ \mathbf {L}_{\mathrm {VAE}}(\theta ,\phi ) \approx \frac {1}{N} \sum _{i=1}^{N} \left [ \mathbb {E}_{p(\epsilon )} \left [ \log p_\theta \left ( x_i \mid \mu _\phi (x_i) + \sigma _\phi (x_i)\odot \epsilon \right ) \right ] + \frac {1}{2} \sum _{k=1}^{b} \left ( 1 + \log \sigma _k^2(x_i,\phi ) - \mu _k^2(x_i,\phi ) - \sigma _k^2(x_i,\phi ) \right ) \right ] \]

Using a single Monte Carlo sample

\[ \epsilon _i\sim \mathcal {N}(0,I) \]

we obtain

\[ z_i = \mu _\phi (x_i) + \sigma _\phi (x_i)\odot \epsilon _i \]

and the objective becomes

\[ \mathbf {L}_{\mathrm {VAE}}(\theta ,\phi ) = \frac {1}{N} \sum _{i=1}^{N} \left [ \log p_\theta (x_i\mid z_i) + \frac {1}{2} \sum _{k=1}^{b} \left ( 1 + \log \sigma _k^2(x_i,\phi ) - \mu _k^2(x_i,\phi ) - \sigma _k^2(x_i,\phi ) \right ) \right ] \]

This objective is then trained using SGD.

2.5 The Generative Adversarial Network (GAN) Framework

GANs, or Generative Adversarial Networks (and many other generative and machine learning models like diffusion models or autoencoders) are motivated by the manifold hypothesis concept, which states that the data of interest (e.g natural images or celebrity faces) form a thin d-dimensional submanifold \(\mathbb {M}\) embedded in a high-dimensional ambient space \(\mathbb {X}\) (e.g \(\mathbb {R}^D\), where \(d<D\); d is known as intrinsic dimension while \(D\) is the ambient dimension).

GAN tries to learn a mapping from a given latent space \(\mathbb {Z}\) to \(\mathbb {M}\) and then samples from \(\mathbb {M}\) by sampling from \(\mathbb {Z}\).

In GAN, there is no explicit likelihood maximization as in a VAE (i.e it is not a likelihood-based model). Instead, there is a generator \(g_\theta (z)\) that generates a candidate sample \(x_{\text {fake}} = g_\theta (z)\), where \(g_\theta \) is a deep neural network.

PIC

The generator is trained to fool a discriminator (binary classifier) \(d_\phi \) in an adversarial fashion. The discriminator, represented by \(d_\phi \), outputs the probability that a sample is real. The goal of the discriminator is to maximize \(\log d_\phi (x)\) for real samples \(x \sim p_{\text {data}}\), and to maximize \(\log \left (1-d_\phi (g_\theta (z))\right )\) for fake samples \(z \sim p(z)\).

The loss is the standard binary cross-entropy loss (i.e., similar to training a binary classifier) and is given by (which is equivalent to minimizing the Jensen-Shannon divergence)

\[ \min _\theta \max _\phi \mathbf {L}^{p_{\text {data}}}_{\text {GAN}}(\theta ,\phi ) = \mathbb {E}_{x\sim p_{\text {data}}} \left [ \log d_\phi (x) \right ] + \mathbb {E}_{z\sim p(z)} \left [ \log \left ( 1-d_\phi (g_\theta (z)) \right ) \right ] \]

As usual, we do not have access to the true data distribution, and thus we approximate it using the empirical distribution:

\[ \min _\theta \max _\phi \mathbf {L}_{\text {GAN}}^{\hat {p}_{\text {data}}}(\theta ,\phi ) = \frac {1}{n}\sum _{i=1}^{n} \left [ \log d_\phi (x_i) \right ] + \mathbb {E}_{z\sim p(z)} \left [ \log \left ( 1-d_\phi (g_\theta (z)) \right ) \right ] \]

The term \(\mathbb {E}_{z\sim p(z)}\) is approximated using Monte Carlo sampling. During SGD training, when training the discriminator, the generator parameters are kept fixed, i.e., the generator does not learn. Similarly, when training the generator, the discriminator parameters are kept fixed. With PyTorch, this alternating training procedure is straightforward to implement.

Getting a GAN to work can be difficult due to vanishing gradients, non-convergence, instability of the adversarial equilibrium, and especially mode collapse. Mode collapse can happen when the generator learns a distribution concentrated on only a few modes or examples instead of approximating the full data distribution.

2.6 Autoregressive and Language Models

2.6.1 Autoregressive Modeling

Autoregressive models are generative models that attempt to learn the underlying data distribution \(p_{\mathrm {data}}(x)\) from which the observed data are assumed to have been generated. The data is usually sequential, such as text represented as tokens; in this scenario, the model is trained to learn the token distribution. Because text is viewed as a sequence of tokens belonging to a vocabulary, the model learns the distribution that governs the world language space. The joint distribution is given by:

\[ p_\theta (x)= p_\theta (x_1) p_\theta (x_2\mid x_1)\cdots p_\theta (x_n\mid x_1,\ldots ,x_{n-1})= p_\theta (x_1) \prod _{k=2}^{n} p_\theta (x_k\mid x_{<k}) \]

where \(x_{<k}=(x_1,\ldots ,x_{k-1})\). Thus, instead of directly learning \(p_\theta (x)\), we learn the conditional distributions; this is why LLMs are said to model the conditional distribution of the next token. Each conditional distribution \(p_\theta (x_k \mid x_{<k})\) is parameterized by the underlying language model with parameters \(\theta \), typically using the final-layer contextual representation at position \(k-1\) to predict the distribution over \(x_k\), that is:

\[ p_\theta (x_t \mid x_{<t}) = f_\theta (x_{<t}) = \left ( \operatorname {softmax} \left ( W_{\mathrm {out}} h_{t-1}^{(L)} + b_{\mathrm {out}} \right ) \right )_{x_t} \]

where \(h_{t-1}^{(L)}\) is the final-layer contextual representation at position \(t-1\).

To make the estimation problem amenable and parameter-efficient, large language models use the same model parameters \(\theta \) to estimate all these conditional distributions. The model is trained using maximum likelihood estimation by maximizing the likelihood of the observed data, or equivalently by minimizing the negative log-likelihood. This is equivalent, up to a constant independent of \(\theta \), to minimizing \(D_{\mathrm {KL}}\left (p_{\mathrm {data}}(x)\,\|\,p_\theta (x)\right )\).

The log-likelihood objective subject to maximization is given by:

\[ \mathbf {L}(\theta )=\mathbb {E}_{x\sim p_{\mathrm {data}}}\left [\sum _{i=1}^{T}\log p_\theta (x_i\mid x_{<i})\right ] \text { , and} \qquad \theta ^*=\arg \max _\theta \mathbf {L}(\theta ) \]

Instead of solving a maximization problem, one can minimize the negative log-likelihood (next-token prediction loss):

\[ \mathbf {L}(\theta )=-\mathbb {E}_{x\sim p_{\mathrm {data}}}\left [\sum _{i=1}^{T}\log p_\theta (x_i\mid x_{<i})\right ] \]

The expectation is approximated using Monte Carlo with samples from the training corpus, that is, the objective or loss becomes:

\[ \min _{\theta }- \frac {1}{|\mathbb {B}|}\sum _{x \in \mathbb {B}}\sum _{i=1}^{T}\log p_\theta (x_i\mid x_{<i}) \]

Where \(\mathbb {B}\) is a randomly picked batch.

Once the conditional distributions \(p_\theta (x_k\mid x_{<k})\) have been learned, their product defines \(p_\theta (x)\). The model can also be used to predict and sample the next token given a context. The sampled token is appended to the context, and this process is recursively repeated until a stopping condition is reached (e.g maximum length or end of sentence token). This is how an autoregressive LLM basically generates text.

Modern language models typically use Transformer architectures to parametrize these conditional distributions. Each \(p_\theta (x_k\mid x_{<k})\) is a categorical distribution over the vocabulary (i.e which tokens are more likely to be picked next). The Transformer produces a final-layer contextualized representation at position \(k-1\):

\[ h_{k-1}^{(L)} = \operatorname {Transformer}_{\theta }(x_{<k})_{k-1} \]

which has attended to the previous tokens and therefore encodes the preceding context. The network then produces one logit for each possible token by projecting this contextualized representation onto the vocabulary, and a softmax transforms these logits into probabilities:

\[ p_\theta (x_k=i\mid x_{<k}) = \operatorname {softmax} \left ( W_{\mathrm {out}}h_{k-1}^{(L)} + b_{\mathrm {out}} \right )_i \]

We will delve into the details of the Transformer architecture later.

2.6.2 Autoregressive Generation

By learning each conditional distribution \(p_\theta (x_k\mid x_{<k})\), the model learns to predict the next token given a context \(x_{<k}\). During generation, a token is produced, appended to the context, and the model is invoked again until either an end-of-sentence (EOS) token is produced or predefined maximum length reached. This process is called autoregressive generation.

During training, teacher forcing is used to enable parallel computation, that is, compute the conditional probabilities ahead of time as next tokens are known (cf tranformer section). During inference, however, generation is sequential and the auto regressive property must be obeyed conservatively because the next tokens are not known.

When using LLM APIs, you will probably come across the notion of temperature; it is basically a way to drive and control the generation process, e.g., from more greedy/deterministic generation to more diverse generation (in other words it control the softness of the distribution). Let \(\boldsymbol {z}_\theta (x_{<k})\) denote the logit vector produced by the model for the token position at \(k\) (as explained earlier), \(\boldsymbol {z}_\theta (x_{<k}) \in \mathbb {R}^{|V|}\), e.g., the Transformer output where the token has the chance to look at preceding token representations. Temperature modifies the sampling distribution according to:

\[ p_{\theta ,\tau }(x_k\mid x_{<k}) = \operatorname {softmax}\left ( \frac {\boldsymbol {z}_\theta (x_{<k})}{\tau } \right )_{x_k} \]

where \(\tau >0\) is the temperature scaling factor. For \(\tau =1\), we obtain the original token model distribution. For \(0<\tau <1\), the distribution becomes sharper, so high-probability tokens are favored more strongly and low-probability tokens are sampled less often, as the differences between the logits are amplified. As \(\tau \rightarrow 0^+\), the distribution concentrates on the token with the largest logit, converging towards greedy decoding. For \(\tau >1\), the distribution becomes flatter, and as \(\tau \rightarrow +\infty \), it tends toward a uniform distribution. Setting \(\tau =0\) is typically interpreted as greedy decoding, that is, the token with the largest logit is selected; it effectively gets a probability of \(1\), while all others get \(0\).

2.6.3 Tokenizer

Given an alphabet \(\Sigma \), that is, a finite set of symbols, let \(\Sigma ^*\) denote the set of all possible finite sequences or strings over the alphabet. A vocabulary \(V\) is a finite set of strings on a given alphabet \(\Sigma \), that is, \(V \subset \Sigma *\).

PIC

A tokenizer, denoted by \(\boldsymbol {\tau }\), breaks down a text into a sequence of tokens belonging to a target vocabulary \(V\). Formally,

\[ \boldsymbol {\tau } : \Sigma ^* \to V^* \]

where \(\Sigma \) denotes the alphabet of characters or bytes \(\{0,\ldots ,255\}\) (if we consider UTF-8 encoding), and \(V\) determines the set of all possible tokens, that is, the vocabulary which may contain words, subwords, individual characters and punctuation symbols ; which is different from usual linguistic vocabulary. In LLMs, the target vocabulary is typically learned from a training corpus.

Given a text, the tokenizer maps it to a sequence of tokens:

\[ \boldsymbol {\tau }(c_1,\ldots ,c_m)=(t_1,\ldots ,t_\ell )\in V^\ell \]

Next, each of these tokens can be mapped to an index using a deterministic indexing function

\[ \boldsymbol {\phi }:V\to \{1,\ldots ,|V|\} \]

Each token ID is then used to index an embedding table, called the token embedding table, which provides a vector representation of the token (which we will discuss in more detail later).

Thus, a tokenizer block takes a string over an alphabet and maps it to a sequence of token indices, whose length \(\ell \) is the tokenized sequence length. If this sequence is used as the model’s input, \(\ell \) is its input context length. Each token is then embedded into a dense vector in a \(d\)-dimensional Euclidean space. Let

\[ \boldsymbol {E}:\{1,\ldots ,|V|\}\to \mathbb {R}^d \]

denote the embedding lookup. Then, for

\[ \boldsymbol {\tau }(c_1,\ldots ,c_m)=(t_1,\ldots ,t_\ell ) \]

the corresponding token embedding matrix can be written as

\[ \boldsymbol {X} := \begin {bmatrix} \boldsymbol {E}(\boldsymbol {\phi }(t_1)) & \boldsymbol {E}(\boldsymbol {\phi }(t_2)) & \cdots & \boldsymbol {E}(\boldsymbol {\phi }(t_\ell )) \end {bmatrix} \in \mathbb {R}^{d\times \ell } \]

Thus, the text is mapped to \(\mathbb {R}^{d\times \ell }\), where each column is the vector representation of one of the \(\ell \) tokens. Here, \(\ell \) is the input sequence length, and \(\boldsymbol {X}\in \mathbb {R}^{d\times \ell }\) is the token embedding matrix that is fed into the machine learning model or LLM. Examples of tokenization algorithms include WordPiece, Unigram Language Model (UnigramLM), and Byte Pair Encoding (BPE).

Some special tokens are also part of the tokenizer’s vocabulary, including , , and , which mark the beginning of a sequence, end of a sequence, and padding, respectively. During supervised fine-tuning and/or reinforcement learning from human feedback (RLHF), where a pre-trained LLM is empowered to follow instructions, each input or conversation message is segmented before being tokenized and then entering the LLM cascade. Some special tokens are introduced to segment dialogue turns. Basically, each input is delimited by <|im_start|> and <|im_end|>. Each message is attached to a role control token, e.g., <|im_start|>role, where the role could be <|system|> (persona and system instructions role), <|user|> (user input role), or <|assistant|> (model generation role), followed by the content of the message.

<|im_start|>{{role}} 
{{content}}<|im_end|>

That is, the mapping from raw data

[ 
  {'role': 'system', 'content': 'You are a helpful security analyst'}, 
  {'role': 'user', 'content': 'analyze security event...'}, 
  {'role': 'assistant', 'content': 'Sure, here is...'} 
]

could be

<|im_start|><|system|> 
You are a helpful security analyst.<|im_end|> 
<|im_start|><|user|> 
analyze security event...<|im_end|> 
<|im_start|><|assistant|> 
Sure, here is...<|im_end|>

Of course, each LLM may have its own chat template. Usually, you do not need to care about the conversation formatting if you use LLM provider APIs or SDKs rather than an open-source model; you just send the conversation. This is, by the way, how you can build LLM-based agents, as covered later.

Large language models are also post-trained on tool usage or function calling, where a list of tool descriptions is presented and the model, based on the task encoded in the instruction and the payload, suggests one or more tool calls. The emitted tool calls can follow a certain schema dictated in the prompt, or special tokens can also be introduced for that purpose. For instance, Qwen (https://qwen.readthedocs.io/en/latest/getting_started/concepts.html) uses XML tags and does not introduce special tool-call tokens. Here is what the template looks like (once the chat template is applied, the tokenizer then comes into play to produce the token sequence, as explained earlier):

<|im_start|>system 
# Tools 
You may call one or more functions to assist with the user query. 
You are provided with function signatures within <tools>tools> XML tags: 
<tools> 
{{JSON Schema of function 1}} 
{{JSON Schema of function 2}} 
tools> 
For each function call, return a JSON object with function name and arguments 
within <tool_call>tool_call> XML tags: 
<tool_call> 
{"name": <function-name>, "arguments": <args-json-object>} 
tool_call> 
<|im_end|> 
<|im_start|>user 
{{user content}} 
<|im_end|> 
<|im_start|>assistant 
<tool_call> 
{{tool call 1}} 
tool_call> 
<tool_call> 
{{tool call 2}} 
tool_call> 
<|im_end|> 
<|im_start|>user 
<tool_response> 
{{tool result 1}} 
tool_response> 
<tool_response> 
{{tool result 2}} 
tool_response> 
<|im_end|> 
<|im_start|>assistant 
{{assistant content}} 
<|im_end|>

On the other hand, Gemma 4 uses special tool tokens rather than XML tags (https://ai.google.dev/gemma/docs/core/prompt-formatting-gemma4); it uses six special tokens to manage the tool-use lifecycle.

A model naturally cannot actually call the tool itself (it samples next probable token or response). Instead, it requests a tool call, and it is the responsibility of the surrounding system to handle the call and append the tool call and its result to the chat history (harness is often used to refer to how tools and other resources like skills or components like neural memory module are organized or orchestrated and connected to the foundation model).

PIC

Additionally, especially when developing agents, large language models can use a thinking mode in which the model emits structured reasoning (...\ or ...\) before answering, e.g., with function calling, where the model reasons about tool usage before making the actual call. For example, Qwen uses and (XML tags, not special tokens) to separate the thinking content:

<|im_start|>user 
{{user content}} 
<|im_end|> 
 
<|im_start|>assistant 
<think> 
{{thinking content}} 
think> 
 
{{assistant content}} 
<|im_end|>

On the other hand, Gemma 4 introduces the <|think|> special token to activate thinking and delimits internal reasoning content using special delimiters or tokens:

<|turn>system 
<|think|>You are a helpful assistant. 
<|tool>declaration:get_current_temperature{...}<tool|> 
<turn|> 
 
<|turn>user 
What's the temperature in London? 
<turn|> 
 
<|turn>model 
<|channel>thought 
... 
<channel|> 
<|tool_call>call:get_current_temperature{ 
    location:<|"|>London<|"|> 
}<tool_call|> 
<|tool_response>

As stated earlier, the chain-of-thought can be enclosed between special tags or tokens, such as ... or .... The tokens inside these reasoning delimiters are commonly referred to as reasoning tokens or thinking tokens. Mathematically, letting \(\mathbf {x}\) denote the input context, \(\mathbf {r}\) the generated reasoning tokens, and \(\mathbf {y}\) the final response tokens, the generation process can be represented as:

\[ p_\theta (\mathbf {y},\mathbf {r}\mid \mathbf {x}) = p_\theta (\mathbf {r}\mid \mathbf {x})\, p_\theta (\mathbf {y}\mid \mathbf {r},\mathbf {x}) \]

That is, the model first autoregressively generates the reasoning tokens conditioned on the input context and then generates the final response conditioned on both the input context and the generated reasoning.

2.6.4 Introducing Attention

The limitation of recurrent neural networks was that inputs might be correlated and distant, resulting in the loss of critical context for accurate predictions. LSTM addressed this issue by utilizing memory cells scattered throughout the network to provide additional contextual information. However, even with LSTM, there were still limitations as you had to put your trust in the model. Additionally, encoding the complete semantics of an entire sentence into the hidden state of the last moment of the encoder in vanilla se2seq presents a significant challenge.

To overcome these limitations, the attention mechanism was introduced. The idea behind the attention mechanism is to allow the model to learn how to selectively focus on the information necessary for predicting outputs. It aims to leverage all the information from the past combining different layers with weighted averages. The attention scores are typically learned via a deep neural network.

2.6.5 Attention Mechanism

Given query, key, and value vectors

\[ q_i,k_i,v_i \in \mathbb {R}^{d} \]

we can arrange them column-wise as

\[ Q= \begin {bmatrix} q_1 & \cdots & q_n \end {bmatrix}, \qquad K= \begin {bmatrix} k_1 & \cdots & k_n \end {bmatrix}, \qquad V= \begin {bmatrix} v_1 & \cdots & v_n \end {bmatrix} \]

where

\[ Q,K,V\in \mathbb {R}^{d\times n} \]

We assume that the query vector \(q\) and key vector \(k\) have undergone a transformation, e.g., through a deep neural network, such that \(q^\top k\) provides a similarity score between \(q\) and \(k\).

For a query \(q_i\), we can define its output \(y_i\) as a weighted average of the value vectors using the similarity scores:

\[ y_i = (q_i^\top k_1)v_1+\cdots +(q_i^\top k_n)v_n = \sum _j (q_i^\top k_j)v_j \]

If we want to normalize the weights so that they are positive and sum to one, we apply the softmax function:

\[ \operatorname {softmax}(x)_i = \frac {e^{x_i}}{\sum _j e^{x_j}} \]

Hence,

\[ y_i = \frac {e^{q_i^\top k_1}} {\sum _j e^{q_i^\top k_j}}v_1 +\cdots + \frac {e^{q_i^\top k_n}} {\sum _j e^{q_i^\top k_j}}v_n \]

Since

\[ Q^\top K = \begin {bmatrix} q_1^\top k_1 & \cdots & q_1^\top k_n\\ \vdots & \ddots & \vdots \\ q_n^\top k_1 & \cdots & q_n^\top k_n \end {bmatrix}, \]

we can write

\[ y_i = \operatorname {softmax}(Q^\top K)_{i1}v_1 +\cdots + \operatorname {softmax}(Q^\top K)_{in}v_n. \]

If we stack the outputs column-wise,

\[ Y= \begin {bmatrix} y_1 & \cdots & y_n \end {bmatrix} \]

then

\[ \operatorname {Att}(Q,K,V) = Y = V\left (\operatorname {softmax}(Q^\top K)\right )^\top , \]

where the softmax is applied row-wise.

In scaled dot-product attention (to cope with situations where the dot product is large), we instead use

\[ \operatorname {Att}(Q,K,V) = V \left [ \operatorname {softmax} \left ( \frac {Q^\top K}{\sqrt {d}} \right ) \right ]^\top \]

If we instead use the row-vector convention, the same expression is usually written as

\[ Y = \operatorname {softmax} \left ( \frac {QK^\top }{\sqrt {d}} \right )V \]

Using the column-vector convention, define the similarity score as

\[ \operatorname {sim}(q_i,k_j) = \frac {q_i^\top k_j}{\sqrt {d}} \]

The normalized attention weight is therefore

\[ \alpha _{ij} = \frac { e^{q_i^\top k_j/\sqrt {d}} }{ \sum _{\ell } e^{q_i^\top k_\ell /\sqrt {d}} } \]

with

\[ \alpha _{ij}\geq 0, \qquad \sum _j\alpha _{ij}=1 \]

The coefficient \(\alpha _{ij}\) can be interpreted as

\[ \alpha _{ij} = p(j\mid q_i) \]

or \(p(k_j\mid q_i)\); given a query \(q_i\), it indicates how much attention mass should be granted to position or key \(j\). This is why LLMs are said to learn an attention distribution.

We can therefore write

\[ y_i = \sum _j \alpha _{ij}v_j = \mathbb {E}_{j\sim p(j\mid q_i)}[v_j] \]
2.6.6 Multi-Head Attention

Multi-head attention is an extension of attention in which multiple heads are introduced. Each head can focus on a different aspect of the sequence. Concretely, \(Q\), \(K\), and \(V\) are projected onto different subspaces, allowing the model to learn multiple kinds of relationships in parallel.

Let \(h\) denote the number of attention heads. For head \(i\), define

\[ Q_i=W_q^{(i)}Q, \qquad K_i=W_k^{(i)}K, \qquad V_i=W_v^{(i)}V, \]

where, for a head dimension \(d_h\),

\[ W_q^{(i)}\in \mathbb {R}^{d_h\times d}, \qquad W_k^{(i)}\in \mathbb {R}^{d_h\times d}, \qquad W_v^{(i)}\in \mathbb {R}^{d_h\times d} \]

Each head is then

\[ \operatorname {head}_i = \operatorname {Att}(Q_i,K_i,V_i) \in \mathbb {R}^{d_h\times n} \]

Equivalently,

\[ \operatorname {head}_i = V_i \left [ \operatorname {softmax} \left ( \frac {Q_i^\top K_i}{\sqrt {d_h}} \right ) \right ]^\top \]

The outputs of the heads are concatenated:

\[ \operatorname {Concat} \left [ \operatorname {head}_1,\ldots ,\operatorname {head}_h \right ] \in \mathbb {R}^{hd_h\times n} \]

Finally,

\[ \operatorname {MultiHead}(Q,K,V) = W_o \operatorname {Concat} \left [ \operatorname {head}_1,\ldots ,\operatorname {head}_h \right ] \]

where

\[ W_o\in \mathbb {R}^{d\times hd_h} \]

so that

\[ \operatorname {MultiHead}(Q,K,V) \in \mathbb {R}^{d\times n} \]
2.6.7 Self-Attention

In self-attention, given

\[ X= \begin {bmatrix} x_1 & \cdots & x_n \end {bmatrix} \in \mathbb {R}^{d\times n}, \]

we compute the queries, keys, and values as projections of the same input:

\[ K=W_kX, \qquad Q=W_qX, \qquad V=W_vX \]

More generally, if the query/key dimension is \(d_k\) and the value dimension is \(d_v\),

\[ W_k\in \mathbb {R}^{d_k\times d}, \qquad W_q\in \mathbb {R}^{d_k\times d}, \qquad W_v\in \mathbb {R}^{d_v\times d} \]

and therefore

\[ K,Q\in \mathbb {R}^{d_k\times n}, \qquad V\in \mathbb {R}^{d_v\times n} \]

We then compute scaled dot-product attention:

\[ A(X) = \operatorname {softmax} \left ( \frac {Q^\top K}{\sqrt {d_k}} \right ) \in \mathbb {R}^{n\times n} \]

The scaling is used to prevent the dot products from becoming too large as \(d_k\) increases.

Finally,

\[ \operatorname {Attention}(X) = V \left [ \operatorname {softmax} \left ( \frac {Q^\top K}{\sqrt {d_k}} \right ) \right ]^\top \]

For the multi-head version, for

\[ i=1,2,\ldots ,h, \]

we define

\[ K_i=W_k^{(i)}X, \qquad Q_i=W_q^{(i)}X, \qquad V_i=W_v^{(i)}X \]

where

\[ W_k^{(i)},W_q^{(i)} \in \mathbb {R}^{d_h\times d}, \qquad W_v^{(i)} \in \mathbb {R}^{d_h\times d} \]

Then

\[ \operatorname {head}_i = \operatorname {Attention}(Q_i,K_i,V_i) \in \mathbb {R}^{d_h\times n} \]

and

\[ \operatorname {MultiHead}(X) = W_o \operatorname {Concat} \left [ \operatorname {head}_1,\ldots ,\operatorname {head}_h \right ] \in \mathbb {R}^{d\times n} \]
2.6.8 Masked Self-Attention

Since we are interested in autoregressive modeling, we want to enforce the autoregressive property so that a token at position \(i\) cannot attend to future positions \(j>i\).

We introduce a causal mask

\[ M\in \mathbb {R}^{n\times n} \]

defined by

\[ M_{ij} = \begin {cases} 0, & j\leq i,\\ -\infty , & j>i. \end {cases} \]

For each head, we then compute

\[ A_i = \operatorname {softmax} \left ( \frac {Q_i^\top K_i}{\sqrt {d_h}}+M \right ) \]

Thus, whenever \(j>i\),

\[ M_{ij}=-\infty \]

and consequently

\[ \frac {q_i^\top k_j}{\sqrt {d_h}}+M_{ij} = -\infty \]

After applying the softmax gives 0 (\(e^{-\infty }=0\)), thus attention to future positions is switched off.

The corresponding head output is

\[ \operatorname {head}_i = V_i \left [ \operatorname {softmax} \left ( \frac {Q_i^\top K_i}{\sqrt {d_h}}+M \right ) \right ]^\top \]

Additionally, we can add a residual connection to form the final contextual embeddings:

\[ \operatorname {MaskedMultiHead}(X) = W_o \operatorname {concat} \left [ \operatorname {head}_1,\ldots ,\operatorname {head}_h \right ] + X \]

Finally, we can add a feed-forward neural network and normalization layer:

\[ H_{\mathrm {temp}} = \operatorname {LayerNorm} \left ( \operatorname {MaskedMultiHead}(X) \right ) \]

where the residual connection with \(X\) is already included in \(\operatorname {MaskedMultiHead}(X)\).

The final output is

\[ H_{\mathrm {final}} = \operatorname {LayerNorm} \left ( \operatorname {FFN}(H_{\mathrm {temp}}) + H_{\mathrm {temp}} \right ) \]
2.6.9 Transformer Architecture

The Transformer architecture has become the go-to deep learning model for various tasks. It consists of an encoder and a decoder. The encoder consists of multi-head attention layers and position-wise feed-forward sublayers. In order to compensate for the absence of a cyclic structure inherent in recurrent neural networks, which naturally capture positional information and ordering, Transformers employ positional encodings. The idea is to augment the input embedding with a fixed sinusoidal positional encoding (one can also use learned positional embeddings).

PIC

For position \(i\), the positional encoding is defined by

\[ \operatorname {PE}(i,2j) = \sin \left ( \frac {i}{10000^{2j/d}} \right ) \]

and

\[ \operatorname {PE}(i,2j+1) = \cos \left ( \frac {i}{10000^{2j/d}} \right ) \]

where \(j\) denotes the dimension index in the embedding space and \(d\) is the dimension of the embedding space.

For a token \(w_i\) with embedding

\[ e_i \in \mathbb {R}^{d}, \qquad \operatorname {PE}(i) \in \mathbb {R}^{d}, \]

we define the position-aware embedding as

\[ \mathbf {x}_i = e_i+\operatorname {PE}(i) \]

where

\[ e_i = E_{w_i}, \qquad E \in \mathbb {R}^{d\times |\mathbb {V}|}, \]

is the embedding matrix learned using backpropagation, where each column is the embedding vector associated with a token in the vocabulary \(\mathbb {V}\), and \(E_{w_i}\) denotes the column of \(E\) corresponding to the token index \(w_i\).

If

\[ \operatorname {onehot}(w_i) \in \mathbb {R}^{|\mathbb {V}|} \]

denotes the one-hot representation of the token index \(w_i\), then

\[ e_i = E\,\operatorname {onehot}(w_i) \in \mathbb {R}^{d} \]

For an input sequence of \(n\) tokens, define

\[ O_{h} = \begin {bmatrix} \operatorname {onehot}(w_1) & \operatorname {onehot}(w_2) & \cdots & \operatorname {onehot}(w_n) \end {bmatrix} \in \{0,1\}^{|\mathbb {V}|\times n} \]

The corresponding token-embedding matrix is therefore

\[ X = EO_{h} \in \mathbb {R}^{d\times n}, \]

where

\[ X = \begin {bmatrix} e_1 & e_2 & \cdots & e_n \end {bmatrix} \]

Thus, the gradient with respect to the embedding matrix \(E\) can be written as

\[ \frac {\partial \mathbf {L}}{\partial E} = \frac {\partial \mathbf {L}}{\partial X} O_{h}^\top \]

where

\[ \frac {\partial \mathbf {L}}{\partial X} \in \mathbb {R}^{d\times n}, \qquad O_{h}^\top \in \mathbb {R}^{n\times |\mathbb {V}|} \]

and consequently

\[ \frac {\partial \mathbf {L}}{\partial E} \in \mathbb {R}^{d\times |\mathbb {V}|} \]

Instead of explicitly constructing the entire sparse one-hot matrix \(O_h\), observe that

\[ E\,\operatorname {onehot}(w_i) = E_{w_i} \]

and thus the forward pass can simply select the corresponding columns of \(E\). Similarly, during the backward pass, gradients only need to be accumulated into the columns that were selected during the forward pass, with the gradients summed when the same column is selected multiple times (we will discuss the torch embedding layer later).

In matrix form, if

\[ X= \begin {bmatrix} e_1 & \cdots & e_n \end {bmatrix} \in \mathbb {R}^{d\times n} \]

and

\[ W_{\mathrm {pos}}^{e} = \begin {bmatrix} p_1 & \cdots & p_n \end {bmatrix} \in \mathbb {R}^{d\times n} \]

then

\[ H_e^{(0)} = X+W_{\mathrm {pos}}^{e} \]

Transformers can be stacked, forming \(N\) layers. The encoder can be written as

\[ H_e^{\prime (i)} = \operatorname {LayerNorm} \left ( \operatorname {MultiHead} \left ( H_e^{(i-1)} \right ) + H_e^{(i-1)} \right ), \]

followed by

\[ H_e^{(i)} = \operatorname {LayerNorm} \left ( \operatorname {FFN} \left ( H_e^{\prime (i)} \right ) + H_e^{\prime (i)} \right ), \qquad i=1,\ldots ,N \]

The final contextual embeddings at the output of the last encoder layer are

\[ H_e^{(N)} = \begin {bmatrix} h_{e,1}^{(N)} & \cdots & h_{e,n}^{(N)} \end {bmatrix} \in \mathbb {R}^{d\times n} \]

The decoder block acts as an autoregressive language model. Its initial representation is

\[ H_d^{(0)} = O+W_{\mathrm {pos}}^{d}, \]

where

\[ W_{\mathrm {pos}}^{d}\in \mathbb {R}^{d\times m} \]

contains the positional embeddings and

\[ O\in \mathbb {R}^{d\times m} \]

contains the output embeddings generated so far, or can also be set to the ground-truth target sequence during training using teacher forcing. The first sublayer is masked multi-head self-attention:

\[ H_d^{\prime (i)} = \operatorname {LayerNorm} \left ( \operatorname {MaskedMultiHead} \left ( H_d^{(i-1)} \right ) + H_d^{(i-1)} \right ) \]

We then use attention over the encoder representations, allowing the decoder to incorporate information from the input sequence and its own previous output:

\[ H_d^{\prime \prime (i)} = \operatorname {LayerNorm} \left ( \operatorname {MultiHead} \left ( H_d^{\prime (i)}, H_e^{(N)}, H_e^{(N)} \right ) + H_d^{\prime (i)} \right ) \]

Finally, the feed-forward sublayer gives

\[ H_d^{(i)} = \operatorname {LayerNorm} \left ( \operatorname {FFN} \left ( H_d^{\prime \prime (i)} \right ) + H_d^{\prime \prime (i)} \right ), \qquad i=1,\ldots ,N. \]

The position-wise feed-forward network is given by

\[ \operatorname {FFN}(x) = W_2\operatorname {ReLU}(W_1x+b_1)+b_2 \]

The layer normalization operation is defined as

\[ \operatorname {LayerNorm}(x) = \gamma \odot \frac {x-\mu }{\sigma } + \beta \]

The final contextual embedding is therefore

\[ H_d^{(N)} = \begin {bmatrix} h_{d,1}^{(N)} & \cdots & h_{d,m}^{(N)} \end {bmatrix} \in \mathbb {R}^{d\times m} \]

This representation can be fed to a classification layer to obtain the next-token predictions. In token prediction, we use

\[ Y = \operatorname {softmax} \left ( W_s H_d^{(N)} + b_s \right ) \in \mathbb {R}^{N_{\mathrm {vocab}}\times m} \]

where

\[ W_s\in \mathbb {R}^{N_{\mathrm {vocab}}\times d}, \qquad b_s\in \mathbb {R}^{N_{\mathrm {vocab}}}. \]

At each position \(i-1\), the final contextual representation \(h_{d,i-1}^{(N)}\), which encodes the preceding context \(w_{<i}=(w_1,\ldots ,w_{i-1})\), is used to predict the next token \(w_i\):

\[ p_\theta (w_i=k\mid w_{<i}) = \operatorname {softmax} \left ( W_s h_{d,i-1}^{(N)}+b_s \right )_k \]

The predicted token ID is obtained as the index of the largest logit, i.e.,

\[ \hat {w}_i = \arg \max _k \left ( W_s h_{d,i-1}^{(N)} + b_s \right )_k \]

The decoder is trained to generate the sentence shifted by one position, effectively learning how to predict the next token. This is the autoregressive decoder used, for example, in GPT-type models.

At position \(t-1\), the final hidden state \(h_{t-1}\), which represents the preceding context \(x_{<t}=(x_1,\ldots ,x_{t-1})\), is projected to one logit per vocabulary token:

\[ z_t = W_s h_{t-1} + b_s \in \mathbb {R}^{|\mathbb {V}|} \]

Then,

\[ p_\theta (\cdot \mid x_{<t}) = \operatorname {softmax}(z_t) \in \mathbb {R}^{|\mathbb {V}|} \]

This parametrizes the categorical distribution over the vocabulary for the next token \(x_t\). Therefore, for a particular vocabulary token \(y\),

\[ p_\theta (y \mid x_{<t}) = \left ( \operatorname {softmax}(W_s h_{t-1} + b_s) \right )_y \]

For an autoregressive language model, the probability of a sequence \(x=(x_1,\ldots ,x_T)\) is factorized using the chain rule as

\[ p_\theta (x) = \prod _{t=1}^{T} p_\theta (x_t\mid x_{<t}) \]

The model is pretrained by maximizing the log-likelihood of the training sequences, or equivalently by minimizing the negative log-likelihood (the cross-entropy loss):

\[ \mathbf {L}_{\mathrm {pretrain}}(\theta ) = -\frac {1}{|\mathbb {D}|} \sum _{x\in \mathbb {D}} \log p_\theta (x) \]

Using the autoregressive factorization, this becomes

\[ \mathbf {L}_{\mathrm {pretrain}}(\theta ) = -\frac {1}{|\mathbb {D}|} \sum _{x\in \mathbb {D}} \sum _{t=1}^{T} \log p_\theta (x_t\mid x_{<t}) \]

And, since \(p_\theta (x_t\mid x_{<t})\) corresponds to the softmax entry associated with the ground-truth token \(x_t\),

\[ \mathbf {L}_{\mathrm {pretrain}}(\theta ) = -\frac {1}{|\mathbb {D}|} \sum _{x\in \mathbb {D}} \sum _{t=1}^{T} \log \left ( \operatorname {softmax} \left ( W_s h_{t-1} + b_s \right ) \right )_{x_t} \]

where \(h_0\) is the hidden representation corresponding to a special beginning of sequence token \( x_{<1} = <\texttt {bos}>\).

2.6.10 KV cache

An input string is tokenized, typically after applying a chat template, and then a set of keys and values is created by projecting the input representations using \(W_K\) and \(W_V\). The new representations for each token are then obtained. If multi-head attention is used, this operation is performed for each head and also at each layer.

During inference, one needs to obey the autoregressive property, and thus each new token is produced and then appended to the current context to form the input for the next decoding step, and so on. However, because we are obeying the autoregressive property, the keys and values of previous tokens, obtained through many GEMMs during prefill (and thus compute bound), will not change due to future tokens. Thus, recomputing them (or redoing the prefill) is unnecessary, and the keys and values for each head and each layer can be cached (and queries discarded). We call the collection of cached key and value vectors the KV cache. There are many scenarios that present opportunities for prefix caching, including agentic workloads, where the system prompt, along with tool descriptions and examples, remains the same, or when a new round consists of previously generated trajectories.

PIC

Basically, for each newly produced token now in step \(t\), we need to compute its new key and value. For example, suppose that, at a given layer and head, the cache contains

\[ K_{\mathrm {cached}} = [k_1,\ldots ,k_{t-1}], \qquad V_{\mathrm {cached}} = [v_1,\ldots ,v_{t-1}] \]

Given the dense representation \(\mathbf {x_t}\) of the new token at the input of this layer, we compute

\[ k_t=W_K\mathbf {x_t}, \qquad v_t=W_V \mathbf {x_t}, \qquad q_t=W_Q \mathbf {x_t} \]

The cache is then extended as (we append to keys and values tensors)

\[ K_{\mathrm {cached}} = [k_1,\ldots ,k_{t-1},k_t] \]
\[ V_{\mathrm {cached}} = [v_1,\ldots ,v_{t-1},v_t] \]

We do not need to recompute or cache the queries of the previous tokens, because we only need the current query \(q_t\) to attend to their cached keys and values to predict the next token. This is why we have a KV cache rather than a QKV cache. The attention output for the current token is computed as

\[ h_t = V_{\mathrm {cached}} \left [ \operatorname {softmax} \left ( \frac {q_t^\top K_{\mathrm {cached}}}{\sqrt {d_k}} \right ) \right ]^\top \]

under the column-vector convention.

Once the new hidden representation for the current token is produced by the first layer, \(h_t^{(1)}\), we continue in the same manner through all the Transformer layers:

\[ h_t^{(0)} \rightarrow h_t^{(1)} \rightarrow \cdots \rightarrow h_t^{(L)} \]

At the final layer, the next-token distribution is obtained by applying the language-model head followed by a softmax:

\[ p_\theta (w_{t+1}=i\mid w_{\leq t}) = \left ( \operatorname {softmax} \left ( W_{\mathrm {out}}h_t^{(L)}+b_{\mathrm {out}} \right ) \right )_i \]

We can now see that there are many matrix-vector operations involved during autoregressive decoding. Since only one token is processed at each decoding step, these operations have relatively low arithmetic intensity compared with the prefill phase. Consequently, autoregressive decoding is typically memory-bound.

A typical implementation of KV caching with prefix caching,(e.g vLLM). The basic idea is to choose a fixed block size for cache lookup. A hash is computed for the first block and checked against the cache. If it is found, the corresponding cached KV block can be reused. For the next block, the hash of the previous block is combined with the tokens of the current block to compute a new hash, which is again checked against the cache. The lookup continues until a block misses. In this case, the cached KV blocks before the miss can be reused, while the tokens starting from the first missing block must be processed through the Transformer layers to compute their KV states. A cache salt can also be used to ensure that only requests with the same salt can reuse cached KV blocks. PagedAttention was introduced to solve memory fragmentation in the KV cache by allowing blocks to be allocated as needed. FlashAttention significantly improve the efficiency of the attention computation using tiling, thereby reducing memory accesses to Keys and Values and avoiding materializing the large attention score matrix.

2.6.11 Knowledge Distillation

Knowledge distillation is a model compression technique that consists of transferring knowledge from a large teacher model to a smaller student model by mimicking the teacher’s categorical distribution; this is because the categorical distribution reveals valuable knowledge concerning other classes rather than just the hard labels. Let \(q(y=i\mid x)\) denote the teacher’s probability distribution and \(p_\theta (y=i\mid x)\) the student’s probability distribution. A common knowledge-distillation objective is the cross-entropy from the teacher distribution to the student distribution:

\[ \mathbf {L}_{\mathrm {KD}}(\theta ) = -\sum _i q(y=i\mid x)\log p_\theta (y=i\mid x) \]

which is equivalent to minimizing the KL divergence, since the first term does not depend on \(\theta \):

\[ D_{\mathrm {KL}}\left (q(\cdot \mid x)\,\|\,p_\theta (\cdot \mid x)\right ) = \sum _i q(y=i\mid x) \log \frac {q(y=i\mid x)}{p_\theta (y=i\mid x)} = -H(q)+H(q,p_\theta ) \]

The student probabilities are obtained from its logits through a softmax (both the teacher’s and student’s logits can be scaled by a temperature parameter). The distillation objective is typically combined with the supervised target loss (e.g., cross-entropy):

\[ \mathbf {L}_{\mathrm {total}} = \alpha \mathbf {L}_{\mathrm {CE}} + \beta \mathbf {L}_{\mathrm {KD}} \]

In language modeling or LLMs, the global loss is given by

\[ \mathbf {L}(\theta ) = \mathbb {E}_{x\sim \mathbb {D}} \left [ \frac {1}{T} \sum _{t=1}^{T} -\log p_\theta (x_t\mid x_{<t}) \right ] + \beta \mathbb {E}_{x\sim \mathbb {D}} \left [ \frac {1}{T} \sum _{t=1}^{T} D_{\mathrm {KL}} \left ( q(\cdot \mid x_{<t}) \,\|\, p_\theta (\cdot \mid x_{<t}) \right ) \right ] \]

where the second loss term is a KL-divergence penalty. Since the teacher distribution \(q\) does not depend on \(\theta \), this term can be written, up to a constant independent of \(\theta \), as a cross-entropy. Therefore, the loss can equivalently be written as

\[ \mathbf {L}(\theta ) = \mathbb {E}_{x\sim \mathbb {D}} \left [ \frac {1}{T} \sum _{t=1}^{T} -\log p_\theta (x_t\mid x_{<t}) \right ] + \beta \mathbb {E}_{x\sim \mathbb {D}} \left [ -\frac {1}{T} \sum _{t=1}^{T} \sum _{v\in \mathbb {V}} q(v\mid x_{<t}) \log p_\theta (v\mid x_{<t}) \right ] \]

If we look at the source code for knowledge distillation (PyTorch Knowledge Distillation Tutorial), the implementation (using full KL divergence) is relatively simple in PyTorch. The teacher forward pass is performed inside torch.no_grad() so that no computation graph is constructed for the teacher, since its parameters are fixed. The student forward pass is performed normally, and the distillation loss is computed directly from the teacher and student logits.

def train_knowledge_distillation(
    teacher,
    student,
    train_loader,
    epochs,
    learning_rate,
    T,
    soft_target_loss_weight,
    ce_loss_weight,
    device
):

    ce_loss = nn.CrossEntropyLoss()
    optimizer = optim.Adam(student.parameters(), lr=learning_rate)

    teacher.eval()   # Teacher set to evaluation mode
    student.train()  # Student set to training mode

    for epoch in range(epochs):

        running_loss = 0.0

        for inputs, labels in train_loader:

            inputs, labels = inputs.to(device), labels.to(device)

            optimizer.zero_grad()

            # Forward pass through the teacher.
            # No computation graph is stored because the teacher is fixed.
            with torch.no_grad():
                teacher_logits = teacher(inputs)

            # Forward pass through the student.
            student_logits = student(inputs)

            # Teacher probabilities and student log-probabilities.
            soft_targets = nn.functional.softmax(
                teacher_logits / T,
                dim=-1
            )

                                                                                     
                                                                                     
            soft_prob = nn.functional.log_softmax(
                student_logits / T,
                dim=-1
            )

            # KL-divergence distillation loss.
            soft_targets_loss = (
                torch.sum(
                    soft_targets
                    * (soft_targets.log() - soft_prob)
                )
                / soft_prob.size()[0]
                * (T**2)
            )

            # Ground-truth cross-entropy loss.
            label_loss = ce_loss(student_logits, labels)

            # Weighted combination of both losses.
            loss = (
                soft_target_loss_weight * soft_targets_loss
                + ce_loss_weight * label_loss
            )

            loss.backward()
            optimizer.step()

            running_loss += loss.item()

        print(
            f"Epoch {epoch+1}/{epochs}, "
            f"Loss: {running_loss / len(train_loader)}"
        )

The same pattern is used for LLM knowledge distillation. For example, in MiniMind’s LLM distillation implementation, the student logits are computed with autograd enabled, whereas the teacher logits are computed without tracking gradients.

def distillation_loss(
    student_logits,
    teacher_logits,
    temperature=1.0,
    reduction='atchmean'):

    with torch.no_grad():
        teacher_probs = F.softmax(
            teacher_logits / temperature,
            dim=-1
        ).detach()

    student_log_probs = F.log_softmax(
        student_logits / temperature,
        dim=-1
    )

    kl = F.kl_div(
        student_log_probs,
        teacher_probs,
        reduction=reduction
    )

    return (temperature ** 2) * kl

During training, the student forward pass is performed first:

with autocast_ctx:
    res = model(input_ids)
    student_logits = (
        res.logits[..., :-1, :]
        .contiguous()
    )

The teacher forward pass is then performed with autograd disabled:

if teacher_model is not None:

    with torch.no_grad():

        teacher_logits = (
            teacher_model(input_ids)
            .logits[..., :-1, :]
            .contiguous()
        )

        vocab_size_student = student_logits.size(-1)

        teacher_logits = (
            teacher_logits[..., :vocab_size_student]
        )

The ground-truth next-token cross-entropy loss is computed from the student logits:

shift_labels = labels[..., 1:].contiguous()
loss_mask_flat = loss_mask.view(-1)

ce_loss = F.cross_entropy(
    student_logits.view(
        -1,
        student_logits.size(-1)
    ),
    shift_labels.view(-1),
    ignore_index=-100,
    reduction='one')

ce_loss_raw = (
    torch.sum(
        ce_loss * loss_mask_flat
    )
    / (loss_mask_flat.sum() + 1e-8)
)

The distillation loss is then computed from the student and teacher logits:

distill_loss = distillation_loss(
    student_logits.view(
        -1,
        student_logits.size(-1)
    )[loss_mask_flat == 1],

    teacher_logits.view(
        -1,
        teacher_logits.size(-1)
    )[loss_mask_flat == 1],

    temperature=temperature
)

The two losses are combined:

loss = (
    alpha * ce_loss
    + (1 - alpha) * distill_loss
) / args.accumulation_steps

and the backward pass is executed only through the student computation graph:

scaler.scale(loss).backward()

followed by the optimizer update:

scaler.step(optimizer)
scaler.update()
optimizer.zero_grad(set_to_none=True)

In LLM distillation, both models receive the same token sequence, but only the student forward pass constructs a computation graph.

2.6.12 MoE and Conditional Computation

The idea behind a Mixture of Experts (MoE) layer is to avoid activating all expert subnetworks, typically FFNs, for every token. Instead, each token is routed to only a small number of experts (possibly only one), and their outputs are combined.

Consider \(E\) experts, each represented by \(\operatorname {FFN}_i\), for \(i=1,\ldots ,E\), together with a router. Given an input \(\mathbf {x}\in \mathbb {R}^d\) (e.g. a latent token representation encoding the previous context), the router first computes logits:

\[ \mathbf {r}(x)=\mathbf {W}_r\mathbf {x}, \qquad \mathbf {W}_r\in \mathbb {R}^{E\times d} \]

These logits are then used to parameterize the router probability distribution by applying the softmax operator, \(\operatorname {softmax}(\mathbf {r}(x))\in \mathbb {R}^{E}\). Let \(\mathbf {S}(x)=\operatorname {TopK}(\mathbf {r}(x),k)\) denote the indices of the \(k\) selected experts. The routing weights can then be defined by masking the non-selected logits to \(-\infty \) and applying softmax (one can avoid materializing the \(-\infty \) values by operating directly on the \(k\) selected logits, i.e by applying Top-\(k\) followed by softmax):

\[ \mathbf {R}(x) = \operatorname {softmax} \left ( \operatorname {MaskTopK}(\mathbf {r}(x)) \right ), \qquad \mathbf {R}_i(x)=0 \quad \text {for } i\notin \mathbf {S}(x) \]

The output of the MoE layer is:

\[ \mathbf {y} = \sum _{i=1}^{E} \mathbf {R}_i(x)\operatorname {FFN}_i(x) = \sum _{i\in \mathbf {S}(x)} \mathbf {R}_i(x)\operatorname {FFN}_i(x) \]

Therefore, only the selected experts need to perform their FFN computations for that token. This is the key idea of conditional computation in sparse MoE models.

To mitigate expert load imbalance, an auxiliary load-balancing loss is commonly introduced. For a batch \(\mathbb {B}\) containing \(T\) tokens, define the fraction of routing assignments sent to expert \(i\) as:

\[ f_i = \frac {1}{kT} \sum _{x\in \mathbb {B}} \mathbf {1}\{i\in \mathbf {S}(x)\}, \qquad \sum _{i=1}^{E}f_i=1 \]

Define the average router probability assigned to expert \(i\) before top-\(k\) masking as:

\[ P_i = \frac {1}{T} \sum _{x\in \mathbb {B}} p_i(x) \]

where \( p_i(x) = \left (\operatorname {softmax}(\mathbf {W}_r x)\right )_i = \frac {\exp (r_i(x))} {\sum _{j=1}^{E}\exp (r_j(x))} \). The auxiliary objective has the form:

\[ \mathbf {L}_{\mathrm {aux}} = \alpha E\sum _{i=1}^{E}f_iP_i \]

Minimizing the above term encourages a more balanced distribution of tokens across experts.

If we look at the MiniMind implementation below, the routing scores are first computed as explained earlier:

scores = F.softmax(self.gate(x_flat), dim=-1)

This is followed by the top-\(k\) expert selection:

topk_weight, topk_idx = torch.topk(
    scores,
    k=self.config.num_experts_per_tok,
    dim=-1,
    sorted=False
)

The selected experts, whose computations will be part of the computation graph, are then invoked on the tokens routed to them, that is, batching tokens per expert, the index_add_ is a differentiable tensor operator that only updates the concerned columns of the target tensor, and its backward formula is straightforward, similar to embedding layer where upstream gradient are accumulated at exact same index slots:

y.index_add_(
    0,
    token_idx,
    (expert(x_flat[token_idx]) * weight).to(y.dtype)
)

Finally, the auxiliary load-balancing loss discussed earlier is also computed, as shown in the complete implementation below:

class MOEFeedForward(nn.Module):

    def __init__(self, config: MiniMindConfig):
        super().__init__()
        self.config = config

        self.gate = nn.Linear(
            config.hidden_size,
            config.num_experts,
            bias=False
        )

        self.experts = nn.ModuleList([
            FeedForward(
                config,
                intermediate_size=config.moe_intermediate_size
            )
            for _ in range(config.num_experts)
        ])

        self.act_fn = ACT2FN[config.hidden_act]

    def forward(self, x):

        batch_size, seq_len, hidden_dim = x.shape
        x_flat = x.view(-1, hidden_dim)

        scores = F.softmax(
            self.gate(x_flat),
            dim=-1
        )

        topk_weight, topk_idx = torch.topk(
            scores,
            k=self.config.num_experts_per_tok,
            dim=-1,
            sorted=False
        )

        if self.config.norm_topk_prob:
            topk_weight = topk_weight / (
                topk_weight.sum(dim=-1, keepdim=True) + 1e-20
                                                                                     
                                                                                     
            )

        y = torch.zeros_like(x_flat)

        for i, expert in enumerate(self.experts):

            mask = (topk_idx == i)

            if mask.any():

                token_idx = (
                    mask.any(dim=-1)
                    .nonzero()
                    .flatten()
                )

                weight = topk_weight[mask].view(-1, 1)

                y.index_add_(
                    0,
                    token_idx,
                    (
                        expert(x_flat[token_idx]) * weight
                    ).to(y.dtype)
                )

            elif self.training:
                y[0, 0] += (
                    0 * sum(
                        p.sum()
                        for p in expert.parameters()
                    )
                )

        if (
            self.training
            and self.config.router_aux_loss_coef > 0
        ):

            load = F.one_hot(
                topk_idx,
                self.config.num_experts
            ).float().mean(0)

            self.aux_loss = (
                (load * scores.mean(0)).sum()
                                                                                     
                                                                                     
                * self.config.num_experts
                * self.config.router_aux_loss_coef
            )

        else:
            self.aux_loss = (
                scores.new_zeros(1).squeeze()
            )

        return y.view(
            batch_size,
            seq_len,
            hidden_dim
        )

When the final model output is computed, all MoE layers are traversed to collect their auxiliary losses, which are summed and added to the main loss before the backward pass.

...
aux_loss = sum([l.mlp.aux_loss for l in self.layers if isinstance(l.mlp, MOEFeedForward)], hidden_states.new_zeros(1).squeeze())
...

res = model(input_ids, labels=labels)
loss = res.loss + res.aux_loss
loss = loss / args.accumulation_steps
scaler.scale(loss).backward()
....

2.6.13 Reinforcement Learning Through Human Feedback (RLTHF)

In this section, we will use reinforcement-learning (RL) terminology. In particular, we denote the conditional distribution \(p_\theta \) by \(\pi _\theta \) and refer to it as the policy (the language model). The goal of reinforcement learning or RLHF is to align the model with human values and teach it for example, to refuse generating harmful content.

PIC

The pretraining loss, where the model learns syntax, grammar and general or world knowledge and problem solving capabilities (i.e encoded in the model parameters), is given by

\[ \min _{\theta } -\frac {1}{|\mathbb {B}|} \sum _{x\in \mathbb {B}} \sum _{t=1}^{T} \log \pi _\theta (x_t\mid x_{<t}) \]

while the supervised fine-tuning (SFT) loss, the capabilities of the model are further improved, specifically the model learns how to follow instructions, generates reasoning traces to solve complex tasks (in addition to improving agentic abilities), is given by

\[ \mathbb {E}_{(x,y)\in \mathbb {D}_{\mathrm {SFT}}} \left [ -\log \pi _\theta (y\mid x) \right ] \]

or

\[ \mathbb {E}_{(x,y)\in \mathbb {D}_{\mathrm {SFT}}} \left [ -\sum _{t=1}^{|y|} \log \pi _\theta \left ( y_t \mid x,y_{<t} \right ) \right ] \]

Where \(\mathbb {D}_{\text {SFT}}\) is instruction following dataset.

For a given minibatch \(\mathbb {B}\), this can be approximated using monte carlo sampling

\[ -\frac {1}{|\mathbb {B}|} \sum _{(x,y)\in \mathbb {B}} \sum _{t=1}^{|y|} \log \pi _\theta \left ( y_t \mid x,y_{<t} \right ) \]

Once the LLM is pretrained and fine-tuned using SFT, a reward model is trained to assign higher scores to outputs that better align with human judgments. The reward model is typically initialized from the pretrained language model by appending a scalar head, e.g., \( r_\phi (x,y)=w^\top h^{\phi }_T([x,y])+b\), where \(h^\phi _T([x,y])\) is the representation produced by the pretrained model (initially \(\phi =\theta \)), typically the last-token representation or contextual embedding, given the prompt \(x\) concatenated with the response \(y\). In Direct Preference Optimization (DPO), the reward model is not explicitly trained.

Let the reward model be denoted by \( R_\psi (x,y)\). Given a prompt \(x\), a preferred response \(y_w\) (winner/chosen response), and a rejected response \(y_l\), the reward model can be trained by minimizing

\[ -\mathbb {E}_{(x,y_w,y_l)\sim \mathbb {D}_{\mathrm {RM}}} \left [ \log \sigma \left ( R_\psi (x,y_w)-R_\psi (x,y_l) \right ) \right ] \]

where \(y_w\) denotes the preferred response and \(y_l\) the rejected response.

Next, we freeze the reward model and train the policy using PPO to maximize the expected cumulative reward (note that a reward is obtained only after the whole response generation, that is, after the eos token is sampled). A KL-regularized objective can initially be written as

\[ \max _{\theta } \; \mathbb {E}_{x\sim \mathbb {X}} \left [ \mathbb {E}_{y\sim \pi _\theta (\cdot \mid x)} \left [ R_\psi (x,y) - \beta D_{\mathrm {KL}} \left ( \pi _\theta (\cdot \mid x) \Vert \pi _{\mathrm {ref}}(\cdot \mid x) \right ) \right ] \right ] \]

Here, \(\pi _{\mathrm {ref}}\) is typically a frozen reference policy, often initialized from the SFT model, and \(\mathbb {X}\) is the prompt distribution.

We now view the problem from the perspective of RL policy optimization. The autoregressive factorization is

\[ \pi _\theta (y\mid x) = \prod _{t=1}^{T} \pi _\theta \left ( y_t\mid x,y_{<t} \right ) \]

We denote

\[ s_t=(x,y_{<t}), \qquad a_t=y_t \]

Thus,

\[ \pi _\theta (y\mid x) = \prod _{t=1}^{T} \pi _\theta (a_t\mid s_t) \]

Let \(x \sim \mathbb {X}\) denote a prompt sampled from the prompt or task distribution (the prompt structure task description and instructions). Let the reward model assign a scalar reward \(R_\psi (x,y)\). In LLM reinforcement learning, the reward can be defined at the token level as

\[ r(y_j,x,y_{<j}) = \begin {cases} R_\psi (x,y), & \text {if } y_j=\texttt {<eos>} \text { or } j=m,\\ 0, & \text {otherwise}. \end {cases} \]

That is, the sequence-level reward is assigned when the final token is produced (or token budget reached), while the intermediate token-level rewards are \(0\). The objective is

\[ \max _\theta \; \mathbb {E}_{x\sim \mathbb {X}} \left [ \mathbb {E}_{y\sim \pi _\theta (\cdot \mid x)} \left [ R(x,y) \right ] \right ] \]

For policy optimization, we introduce the trajectory (where each action \(a_i\) is a token sampled, that is, the action space is the set of possible output tokens)

\[ \tau = (s_0,a_0,s_1,a_1,\ldots ,s_{T-1},a_{T-1}, s_{T}) \]

The probability of a trajectory under the policy is

\[ p_\theta (\tau ) = \rho (s_0) \prod _{t=0}^{T-1} \pi _\theta (a_t\mid s_t) P(s_{t+1}\mid s_t,a_t) \]

where \(\rho (s_0)\) is the initial state distribution over the prompt dataset and

\[ P(s_{t+1}\mid s_t,a_t) \]

is the transition probability. For an autoregressive LLM, the transition is deterministic: appending the generated token \(a_t\) to the current prefix \(s_t\) determines \(s_{t+1}\). That is,

\[ P(s_{t+1}\mid s_t,a_t) = \begin {cases} 1 & \text {if } s_{t+1}=(s_t,a_t),\\ 0 & \text {otherwise}. \end {cases} \]

Let the reward associated with trajectory \(\tau \) be denoted by \(R(\tau )\).

The objective becomes

\[ \max _\theta \; \mathbb {E}_{\tau \sim p_\theta } \left [ R(\tau ) \right ] \]

We could attempt to estimate

\[ \mathbb {E}_{\tau \sim p_\theta } \left [ R(\tau ) \right ] \]

using Monte Carlo samples,

\[ \mathbb {E}_{\tau \sim p_\theta } \left [ R(\tau ) \right ] \approx \frac {1}{N} \sum _{i=1}^{N} R(\tau ^{(i)}) \]

However, after sampling the trajectories, the terms \(R(\tau ^{(i)})\) does not depend on \(\theta \). Instead, we use the log-derivative trick (we faced the same issue in VAE and instead used reparameterization trick but here we can’t):

\[ \nabla _\theta p_\theta (\tau ) = p_\theta (\tau ) \nabla _\theta \log p_\theta (\tau ) \]

Starting from the objective,

\[ \nabla _\theta \mathbb {E}_{\tau \sim p_\theta } \left [ R(\tau ) \right ] = \nabla _\theta \int _{\tau } p_\theta (\tau )R(\tau )\,d\tau \]

Moving the derivative inside,

\[ = \int _{\tau } \nabla _\theta p_\theta (\tau ) R(\tau )\,d\tau \]

Using

\[ \nabla _\theta p_\theta (\tau ) = p_\theta (\tau ) \nabla _\theta \log p_\theta (\tau ) \]

we obtain

\[ = \int _{\tau } p_\theta (\tau ) R(\tau ) \nabla _\theta \log p_\theta (\tau ) \,d\tau \]

Therefore,

\[ \nabla _\theta \mathbb {E}_{\tau \sim p_\theta } \left [ R(\tau ) \right ] = \mathbb {E}_{\tau \sim p_\theta } \left [ R(\tau ) \nabla _\theta \log p_\theta (\tau ) \right ] \]

From

\[ p_\theta (\tau ) = \rho (s_0) \prod _{t=0}^{T-1} \pi _\theta (a_t\mid s_t) P(s_{t+1}\mid s_t,a_t), \]

we have

\[ \log p_\theta (\tau ) = \log \rho (s_0) + \sum _{t=0}^{T-1} \log \pi _\theta (a_t\mid s_t) + \sum _{t=0}^{T-1} \log P(s_{t+1}\mid s_t,a_t) \]

Hence,

\[ \nabla _\theta \log p_\theta (\tau ) = \nabla _\theta \log \rho (s_0) + \sum _{t=0}^{T} \nabla _\theta \log \pi _\theta (a_t\mid s_t) + \sum _{t=0}^{T} \nabla _\theta \log P(s_{t+1}\mid s_t,a_t) \]

The initial-state distribution and environment transition dynamics do not depend on \(\theta \), so

\[ \nabla _\theta \log \rho (s_0)=0 \]

and

\[ \nabla _\theta \log P(s_{t+1}\mid s_t,a_t)=0 \]

Therefore,

\[ \nabla _\theta \log p_\theta (\tau ) = \sum _{t=0}^{T-1} \nabla _\theta \log \pi _\theta (a_t\mid s_t) \]

Substituting this into the policy-gradient expression gives

\[ \nabla _\theta \mathbb {E}_{\tau \sim p_\theta } [R(\tau )] = \mathbb {E}_{\tau \sim p_\theta } \left [ \sum _{t=0}^{T-1} R(\tau ) \nabla _\theta \log \pi _\theta (a_t\mid s_t) \right ] \]

Using a minibatch \(\mathbb {B}\) of sampled trajectories,

\[ \nabla _\theta \mathbb {E}_{\tau \sim p_\theta } [R(\tau )] \approx \frac {1}{|\mathbb {B}|} \sum _{\tau \in \mathbb {B}} \sum _{t=0}^{T} R(\tau ) \nabla _\theta \log \pi _\theta (a_t\mid s_t) \]

Correspondingly, we can define a policy loss of the form

\[ \mathbf {L}(\theta ) = - \mathbb {E}_{\tau \sim p_\theta } \left [ \sum _{t=0}^{T} R(\tau ) \log \pi _\theta (a_t\mid s_t) \right ] \]

with the sampled trajectories treated as fixed when differentiating the Monte Carlo surrogate.

The policy gradient estimator above can have high variance. We can reduce its variance by introducing a baseline \(b(s_t)\), subtracting it from the return or empirical reward. To justify this, we first show that

\[ \mathbb {E}_{\tau \sim p_\theta } \left [ b(s_t) \nabla _\theta \log \pi _\theta (a_t\mid s_t) \right ] =0 \]

Consider the complete trajectory and separate the expectation into the trajectory prefix up to \(s_t\), the current action \(a_t\), and the future trajectory:

\[ \begin {aligned} & \mathbb {E}_{\tau \sim p_\theta } \left [ b(s_t) \nabla _\theta \log \pi _\theta (a_t\mid s_t) \right ] \\ &= \mathbb {E}_{s_0,a_0,\ldots ,s_t} \left [ \mathbb {E}_{a_t} \left [ \mathbb {E}_{s_{t+1},a_{t+1},\ldots ,s_{T-1},a_{T-1}, s_{T}} \left [ b(s_t) \nabla _\theta \log \pi _\theta (a_t\mid s_t) \right ] \right ] \right ] \end {aligned} \]
\[ = \mathbb {E}_{s_0,a_0,\ldots ,s_t} \left [ b(s_t) \mathbb {E}_{a_t\sim \pi _\theta (\cdot \mid s_t)} \left [ \nabla _\theta \log \pi _\theta (a_t\mid s_t) \right ] \right ] \]

Now,

\[ \mathbb {E}_{a_t\sim \pi _\theta (\cdot \mid s_t)} \left [ \nabla _\theta \log \pi _\theta (a_t\mid s_t) \right ] = \sum _{a_t} \pi _\theta (a_t\mid s_t) \nabla _\theta \log \pi _\theta (a_t\mid s_t) \]

Using

\[ \pi _\theta (a_t\mid s_t) \nabla _\theta \log \pi _\theta (a_t\mid s_t) = \nabla _\theta \pi _\theta (a_t\mid s_t) \]

we obtain

\[ = \sum _{a_t} \nabla _\theta \pi _\theta (a_t\mid s_t) \]

Therefore,

\[ = \nabla _\theta \sum _{a_t} \pi _\theta (a_t\mid s_t) \]

Since

\[ \sum _{a_t} \pi _\theta (a_t\mid s_t) = 1 \]

we have

\[ \mathbb {E}_{a_t\sim \pi _\theta (\cdot \mid s_t)} \left [ \nabla _\theta \log \pi _\theta (a_t\mid s_t) \right ] = \nabla _\theta 1 = 0 \]

Consequently,

\[ \mathbb {E}_{\tau \sim p_\theta } \left [ b(s_t) \nabla _\theta \log \pi _\theta (a_t\mid s_t) \right ] = 0, \qquad \forall t \]

Because expectation is linear,

\[ \mathbb {E} \left [ \sum _t X_t \right ] = \sum _t \mathbb {E}[X_t] \]

regardless of whether the \(X_t\)’s are independent. Hence,

\[ \begin {aligned} & \mathbb {E}_{\tau \sim p_\theta } \left [ \sum _{t=0}^{T} b(s_t) \nabla _\theta \log \pi _\theta (a_t\mid s_t) \right ] \\ &= \sum _{t=0}^{T} \mathbb {E}_{\tau \sim p_\theta } \left [ b(s_t) \nabla _\theta \log \pi _\theta (a_t\mid s_t) \right ] \\ &= 0. \end {aligned} \]

Thus, subtracting a state-dependent (or action independent) baseline does not change the expected policy gradient:

\[ \nabla _\theta \mathbb {E}_{\tau \sim p_\theta } [R(\tau )] = \mathbb {E}_{\tau \sim p_\theta } \left [ \sum _{t=0}^{T} \nabla _\theta \log \pi _\theta (a_t\mid s_t) \left ( R(\tau )-b(s_t) \right ) \right ] \]

We can now introduce the loss

\[ \mathbf {L}(\theta ) = - \mathbb {E}_{\tau \sim p_\theta } \left [ \sum _t \log \pi _\theta (a_t\mid s_t) \left ( R(\tau )-b(s_t) \right ) \right ] \]

The baseline \(b(s_t)\) is typically represented using a value function,

\[ b(s_t)=V_\phi (s_t) \]

The value function represents the expected return starting from state \(s_t\)(approximated using a deep neural network).

For state \(s_t\), the difference

\[ R(\tau )-V_\phi (s_t) \]

acts as an advantage estimate. We therefore write

\[ A_t = R(\tau )-V_\phi (s_t) \]

The policy objective can consequently be written in terms of the advantage as

\[ \mathbf {L}(\theta ) = - \mathbb {E}_{\tau \sim p_\theta } \left [ \sum _t \log \pi _\theta (a_t\mid s_t) A_t \right ] \]

The value network \(V_\phi \) is learned using gradient descent, separately from the policy parameters, by minimizing a mean-squared-error objective:

\[ \phi ^\star = \arg \min _\phi \; \mathbb {E}_{\tau \sim p_\theta } \left [ \sum _t \left ( V_\phi (s_t)-R(\tau ) \right )^2 \right ] \]

Now, we still have a problem with the above objective, we want to avoid large policy changes or policy degradation while reusing trajectories sampled from an older policy. We can introduce the importance sampling ratio

\[ r_t(\theta ) = \frac { \pi _\theta (a_t\mid s_t) }{ \pi _{\mathrm {old}}(a_t\mid s_t) } \]

This leads to the surrogate objective

\[ \mathbb {E}_{\tau \sim p_{\theta _{\mathrm {old}}}} \left [ \sum _t r_t(\theta )A_t \right ] \]

A KL penalty can also be added, giving an objective of the form

\[ \mathbb {E} \left [ \sum _t r_t(\theta )A_t - \beta D_{\mathrm {KL}} \left ( \pi _\theta (\cdot \mid s_t) \Vert \pi _{\mathrm {ref}}(\cdot \mid s_t) \right ) \right ] \]

The reference policy \(\pi _{\mathrm {ref}}\) is generally a frozen version of the SFT policy and acts as an anchor that discourages the RL-trained policy from moving too far away from the original language model.

Finally, a clipping mechanism can be added, giving the PPO objective

\[ \mathbf {L}_{\mathrm {PPO}}(\theta ) = - \mathbb {E}_{x\sim \mathbb {X}} \left [ \mathbb {E}_{y\sim \pi _{\mathrm {old}}(\cdot \mid x)} \left [ \frac {1}{|y|} \sum _{t=1}^{|y|} \min \left ( r_t(\theta )A_t, \operatorname {clip} \left ( r_t(\theta ), 1-\epsilon , 1+\epsilon \right ) A_t \right ) \right ] \right ] \]

where

\[ r_t(\theta ) = \frac { \pi _\theta \left ( y_t\mid x,y_{<t} \right ) }{ \pi _{\mathrm {old}} \left ( y_t\mid x,y_{<t} \right ) } \]

and,

\[ A_t = R(x,y) - V_\phi (x,y_{<t}) \]

or

\[ r_t(\theta ) = \frac {\pi _\theta (a_t\mid s_t)} {\pi _{\mathrm {old}}(a_t\mid s_t)} \]

and

\[ A_t = R(x,y)-V_\phi (s_t) \]

The above term is referred to as advantage estimate.

The value function is trained to minimize the prediction error between the estimated value and the observed return:

\[ \mathbf {L}_{\mathrm {value}}(\phi ) = \mathbb {E}_{x\sim \mathbb {X}} \left [ \mathbb {E}_{y\sim \pi _\theta (\cdot \mid x)} \left [ \frac {1}{|y|} \sum _{t=1}^{|y|} \left ( V_\phi (s_t)-R(x,y) \right )^2 \right ] \right ] \]

Thus, the policy is optimized using the PPO clipped surrogate objective, while the value function is trained concurrently to predict the expected return and provide the baseline used to construct the advantage. The algorithm for the PPO is listed below:

Algorithm 1: Proximal Policy Optimization (PPO)
1:   while until convergence do
2:    Collect set of trajectories
  \(\displaystyle \left \{ \left (x^{(i)},y^{(i)}\right ) \right \}_{i=1}^{N}, \qquad x^{(i)}\sim \mathbb {X}, \qquad y^{(i)} \sim \pi _{\theta _{\mathrm {old}}} \left (\cdot \mid x^{(i)}\right ) \)
3:    Compute the advantage estimates \(\hat {A}_t^{(i)}\) based on current value function
  \(\displaystyle \hat {A}_t^{(i)} = R_\psi \left (x^{(i)},y^{(i)}\right ) - V_\phi \left (s_t^{(i)}\right ), \qquad s_t^{(i)} = \left (x^{(i)},y_{<t}^{(i)}\right ) \)
4:    Compute the probability ratios
  \(\displaystyle r_t^{(i)}(\theta ) = \frac { \pi _\theta \left ( y_t^{(i)} \mid s_t^{(i)} \right ) }{ \pi _{\theta _{\mathrm {old}}} \left ( y_t^{(i)} \mid s_t^{(i)} \right ) } \)
5:    Update the policy using stochastic gradient ascent
  \(\displaystyle \theta = \arg \max _{\theta } \sum _{i=1}^{N} \sum _{t=1}^{m^{(i)}} \min \left ( r_t^{(i)}(\theta )\hat {A}_t^{(i)}, \operatorname {clip} \left ( r_t^{(i)}(\theta ), 1-\epsilon , 1+\epsilon \right ) \hat {A}_t^{(i)} \right ) \)
6:    Update the old policy
  \(\displaystyle \theta _{\mathrm {old}} \leftarrow \theta \)
7:    Update the value function by solving (using gradient descent)
  \(\displaystyle \phi \leftarrow \arg \min _{\phi } \frac {1}{N} \sum _{i=1}^{N} \frac {1}{m^{(i)}} \sum _{t=1}^{m^{(i)}} \frac {1}{2} \left ( R_\psi \left (x^{(i)},y^{(i)}\right ) - V_\phi \left (s_t^{(i)}\right ) \right )^2 \)
8:   end while

Algorithm 2: Proximal Policy Optimization (PPO) with KL Penalty
1:   while until convergence do
2:    Collect a set of trajectories
\[ \left \{ \left (x^{(i)},y^{(i)}\right ) \right \}_{i=1}^{N}, \qquad x^{(i)}\sim \mathbb {X}, \qquad y^{(i)} \sim \pi _{\theta _{\mathrm {old}}} \left (\cdot \mid x^{(i)}\right ) \]

3:    Compute the KL-penalized return
\[ \hat {G}_t^{(i)} = R_\psi \left (x^{(i)},y^{(i)}\right ) - \beta \sum _{j=t}^{m^{(i)}} \log \left ( \frac { \pi _{\theta } \left ( y_j^{(i)} \mid s_j^{(i)} \right ) }{ \pi _{\mathrm {ref}} \left ( y_j^{(i)} \mid s_j^{(i)} \right ) } \right ), \qquad s_j^{(i)} = \left (x^{(i)},y_{<j}^{(i)}\right ) \]

4:    Compute the advantage estimates based on the current value function
\[ \hat {A}_t^{(i)} = \hat {G}_t^{(i)} - V_\phi \left (s_t^{(i)}\right ), \qquad s_t^{(i)} = \left (x^{(i)},y_{<t}^{(i)}\right ) \]

5:    Compute the probability ratios
\[ r_t^{(i)}(\theta ) = \frac { \pi _\theta \left ( y_t^{(i)} \mid s_t^{(i)} \right ) }{ \pi _{\theta _{\mathrm {old}}} \left ( y_t^{(i)} \mid s_t^{(i)} \right ) } \]

6:    Update the policy using stochastic gradient ascent
\[ \theta = \arg \max _{\theta } \sum _{i=1}^{N} \sum _{t=1}^{m^{(i)}} \min \left ( r_t^{(i)}(\theta )\hat {A}_t^{(i)}, \operatorname {clip} \left ( r_t^{(i)}(\theta ), 1-\epsilon , 1+\epsilon \right ) \hat {A}_t^{(i)} \right ) \]

7:    Update the old policy
\[ \theta _{\mathrm {old}} \leftarrow \theta \]

8:    Update the value function using gradient descent
\[ \phi \leftarrow \arg \min _{\phi } \frac {1}{N} \sum _{i=1}^{N} \frac {1}{m^{(i)}} \sum _{t=1}^{m^{(i)}} \frac {1}{2} \left ( \hat {G}_t^{(i)} - V_\phi \left (s_t^{(i)}\right ) \right )^2 \]

9:   end while

To better understand how PPO works, let’s look in detail at the implementation provided in the following article: https://cameronrwolfe.substack.com/p/llm-rl.

Given one text sample \((x,y)\), where \(x\) is the prompt and \(y\) is the generated response, let

\[ x=(x_1,\ldots ,x_m), \qquad y=(y_1,\ldots ,y_n) \]

The complete sequence is the concatenation

\[ x\circ y = (x_1,\ldots ,x_m,y_1,\ldots ,y_n) \]

or with the BOS token,

\[ x\circ y = (x_0,x_1,\ldots ,x_m,y_1,\ldots ,y_n) \]

After tokenization and embedding, we obtain

\[ E(x\circ y) = [e_0,\ldots ,e_{m+n}], \qquad e_i\in \mathbb {R}^{d} \]

We pass the complete sequence \(x\circ y\) through the Transformer. Its final layer produces

\[ H = [h_0,\ldots ,h_{m+n}], \qquad h_i\in \mathbb {R}^{d} \]

Because the Transformer is causal, \(h_i\) can depend only on tokens at positions \(0,\ldots ,i\) (we can then think of \(h_i\) as compressing \((x \circ y)_{<i+1}\)). The policy model applies the language-model head to every hidden representation:

\[ \pi _\theta (\cdot \mid (x \circ y)_{<i+1}) = \operatorname {softmax} \left ( W_s h_i+b_s \right ) \in \mathbb {R}^{|\mathbb V|} \]

Therefore, applying the LM head to the complete hidden-state matrix gives

\[ \Pi _\theta = \left [ \pi _\theta (\cdot \mid (x \circ y)_{<1}), \ldots , \pi _\theta (\cdot \mid (x \circ y)_{<m+n+1}) \right ] \in \mathbb {R}^{|\mathbb V|\times (m+n+1)} \]

Here, \((x \circ y)_{<1}\) corresponds to the BOS token. Because the loss operates on the response tokens, we could write:

\[ \pi _\theta (y_t\mid x,y_{<t}) = \operatorname {softmax} \left ( W_s h_{m+t-1}+b_s \right )_{y_t} \]

That is, \(x\) is the initial state of the policy, and \(h_m\) serves as a compression of the prompt for the prediction of the first response token.

The last hidden representation \(h_{m+n}\) contains the complete sequence \((x,y)\), including \(y_n\), and would be used to predict the next token \(y_{n+1}\). However, \(y_{n+1}\) is not part of the stored rollout. Therefore, the last logit is removed in the implementation:

\[ \texttt {logits = logits[:, :-1, :]}. \]

The remaining logits are then aligned with the shifted token IDs

\[ [x_1,\ldots ,x_m,y_1,\ldots ,y_n] \]

We do not need to physically remove the positions corresponding to the prompt \(x\). Instead, we could define the completion mask

\[ M = [ \underbrace {0,\ldots ,0}_{m}, \underbrace {1,\ldots ,1}_{n} ] \in \{0,1\}^{m+n} \]

After applying the LM head, removing the last logit, and gathering the probability corresponding to the actual next-token index at every position (from the dataset), we obtain

\[ \boldsymbol {\ell }_{\theta } = \left [ \log \pi _\theta (x_1\mid x_0), \ldots , \log \pi _\theta (x_m\mid x_{<m}), \log \pi _\theta (y_1\mid x), \ldots , \log \pi _\theta (y_n\mid x,y_{<n}) \right ] \]

Applying the completion mask gives

\[ \boldsymbol {\ell }_{\theta }^{\mathrm {masked}} = \left [ 0,\ldots ,0, \log \pi _\theta (y_1\mid x), \log \pi _\theta (y_2\mid x,y_1), \ldots , \log \pi _\theta (y_n\mid x,y_{<n}) \right ] \]

During rollout generation, we store the log-probabilities under the policy before the PPO updates. Denote this frozen copy during the PPO optimization epochs by \(\pi _{\theta _{\mathrm {old}}}\).

Its gathered token log-probabilities are

\[ \boldsymbol {\ell }_{\mathrm {old}} = \left [ \log \pi _{\theta _{\mathrm {old}}}(x_1\mid x_0), \ldots , \log \pi _{\theta _{\mathrm {old}}}(x_m\mid x_{<m}), \log \pi _{\theta _{\mathrm {old}}}(y_1\mid x), \ldots , \log \pi _{\theta _{\mathrm {old}}}(y_n\mid x,y_{<n}) \right ] \]

The frozen reference model \(\pi _{\mathrm {ref}}\) similarly gives

\[ \boldsymbol {\ell }_{\mathrm {ref}} = \left [ \log \pi _{\mathrm {ref}}(x_1\mid x_0), \ldots , \log \pi _{\mathrm {ref}}(x_m\mid x_{<m}), \log \pi _{\mathrm {ref}}(y_1\mid x), \ldots , \log \pi _{\mathrm {ref}}(y_n\mid x,y_{<n}) \right ] \]

For the rollout, the sampled per-token log-ratio relative to the reference policy (considering only \(y\)) is

\[ \mathrm {KL}_t = \log \frac { \pi _{\theta _{\mathrm {old}}}(y_t\mid x,y_{<t}) }{ \pi _{\mathrm {ref}}(y_t\mid x,y_{<t}) } = (\boldsymbol {\ell }_{\mathrm {old}}^{\mathrm {masked}})_t - (\boldsymbol {\ell }_{\mathrm {ref}}^{\mathrm {masked}})_t \]

In tensor form, once we apply the mask,

\[ \mathbf {KL} = \left [ 0,\ldots ,0, \log \frac { \pi _{\theta _{\mathrm {old}}}(y_1\mid x) }{ \pi _{\mathrm {ref}}(y_1\mid x) }, \ldots , \log \frac { \pi _{\theta _{\mathrm {old}}}(y_n\mid x,y_{<n}) }{ \pi _{\mathrm {ref}}(y_n\mid x,y_{<n}) } \right ] \]

Now recall that the reward-to-go tensor \(\mathbf G\) introduced earlier has the form

\[ G_t = R(x,y) - \beta \sum _{k=t}^{n} \log \frac { \pi _{\theta _{\mathrm {old}}}(y_k\mid x,y_{<k}) }{ \pi _{\mathrm {ref}}(y_k\mid x,y_{<k}) } \]

We can build it by first multiplying the KL terms by \(-\beta \) and adding the reward to the last response token only:

\[ \mathbf {KL}_R = \left [ 0,\ldots ,0, -\beta \log \frac { \pi _{\theta _{\mathrm {old}}}(y_1\mid x) }{ \pi _{\mathrm {ref}}(y_1\mid x) }, \ldots , -\beta \log \frac { \pi _{\theta _{\mathrm {old}}}(y_n\mid x,y_{<n}) }{ \pi _{\mathrm {ref}}(y_n\mid x,y_{<n}) } + R(x,y) \right ] \]

We then perform a reverse torch.cumsum to obtain

\[ \mathbf G = \left [ 0,\ldots ,0, G_1, G_2, \ldots , G_n \right ] \]

The advantage is obtained by subtracting the old value estimate from the corresponding reward-to-go. For each response token,

\[ A_t = G_t - V_{\phi _{\mathrm {old}}}(x,y_{<t}) \]

But the value model processes the complete sequence

\[ x\circ y = (x_0,x_1,\ldots ,x_m,y_1,\ldots ,y_n) \]

and, assuming it uses the hidden representations produced earlier, that is, at every position,

\[ H = [h_0,\ldots ,h_{m+n}] \]

A scalar value head is applied independently to every hidden representation:

\[ V_\phi (h_i) = w_v^\top h_i+b_v, \qquad w_v\in \mathbb {R}^{d} \]

Therefore, the value head can compute all values in parallel through one matrix operation:

\[ \mathbf V_\phi = w_v^\top H+b_v \]

That is,

\[ \mathbf V_\phi = \left [ w_v^\top h_0+b_v, \ldots , w_v^\top h_{m+n}+b_v \right ] \]

Just as with the policy logits, the final value

\[ V_\phi (h_{m+n}) = V_\phi (x,y_1,\ldots ,y_n) \]

corresponds to the state after the final action \(y_n\) and would therefore be associated with the next action \(y_{n+1}\). Since \(y_{n+1}\) is not part of the stored rollout, this final value is not needed for the PPO loss. We therefore use

\[ \left [ V_\phi (h_0), \ldots , V_\phi (h_{m+n-1}) \right ] \]

Then we could mask it:

\[ \mathbf V_\phi ^{\mathrm {masked}} = \left [ 0,\ldots ,0, V_\phi (x), V_\phi (x,y_1), \ldots , V_\phi (x,y_{<n}) \right ] \]

We then obtain our advantage tensor by subtraction:

\[ \mathbf A = \left [ 0,\ldots ,0, A_1, A_2, \ldots , A_n \right ] \]

Now that we have built the advantage and reward-to-go tensor, we could start PyTorch training; that is, we use the stored token IDs produced during trajectory generation to perform a forward pass and obtain a full computation graph for all tokens in parallel in order to train the policy. We compute

\[ \rho _t(\theta ) = \frac { \pi _\theta (y_t\mid x,y_{<t}) }{ \pi _{\theta _{\mathrm {old}}}(y_t\mid x,y_{<t}) } \]

That is,

\[ \boldsymbol {\rho }_\theta = \exp \left ( \boldsymbol {\ell }_\theta - \boldsymbol {\ell }_{\mathrm {old}} \right ) \]

We then apply clipping at the tensor level, and finally multiply the clipped and non-clipped ratios by the advantage tensor and take the minimum. We multiply by the mask again to finally produce the policy loss by taking the sum and thus produce the scalar loss presented in the algorithm listed earlier.

The critic loss is also defined by first producing the values for each token, subtracting the returns tensor produced earlier, that is, \(\mathbf G\), and taking the result to the power of \(2\). We then mask the prompt positions that are excluded from the loss and take the sum. The final loss is

\[ \mathbf L_{\mathrm {PPO}} = \mathbf L_{\mathrm {policy}} + c_v\mathbf L_{\mathrm {value}} \]

Now, the above can be generalized to a batch; the only difference is that, to obtain a scalar loss, we take the mean.

Finally, we execute the backward pass and call the optimizer step to update the parameters. \(X\) epochs are performed before finishing one PPO step. In the next iteration, we set the old policy to the newly updated policy, perform the rollout again, and repeat until convergence. The reference policy \(\pi _{\mathrm {ref}}\) remains frozen.

You can now convince yourself that this is exactly what the code below does. Go through the exercise yourself; it should be very easy to follow now:

import torch 
 
# B - batch size 
# L - sequence length 
# V - vocabulary size 
 
# constants 
kl_beta = 0.1 
critic_weight = 0.5 
ppo_eps = 0.2 
num_ppo_epochs = 4 
 
# sample completions from old policy 
# compute (trajectory-level) rewards 
with torch.no_grad(): 
    completions = policy.generate(prompts) 
    rewards = reward_model(prompts, completions) # (B,) 
 
# input_ids contains prompt + completion tokens 
input_ids = torch.cat([prompts, completions], dim=-1)  # (B, L) 
 
# causal shift 
sampled_ids = input_ids[:, 1:]                   # (B, L-1) 
 
# completion_mask: (B, L-1) 
# masks out prompt and padding tokens 
 
# get logprobs from old policy (policy that generated the rollout) 
# these remain fixed during all PPO updates on this batch 
with torch.no_grad(): 
    old_logits = policy(input_ids)               # (B, L, V) 
    old_logits = old_logits[:, :-1, :]           # (B, L-1, V) 
 
    old_per_token_logps = torch.log_softmax( 
        old_logits, dim=-1 
    ) 
    old_per_token_logps = old_per_token_logps.gather( 
        dim=-1, 
        index=sampled_ids.unsqueeze(-1) 
    ).squeeze(-1)                                # (B, L-1) 
 
# get logprobs of the same actions under reference policy 
with torch.no_grad(): 
    reference_logits = reference_policy(input_ids) 
    reference_logits = reference_logits[:, :-1, :] 
 
    reference_per_token_logps = torch.log_softmax( 
        reference_logits, dim=-1 
    ) 
    reference_per_token_logps = reference_per_token_logps.gather( 
        dim=-1, 
        index=sampled_ids.unsqueeze(-1) 
    ).squeeze(-1)                                # (B, L-1) 
 
# compute critic values for the rollout 
with torch.no_grad(): 
    old_values = critic(input_ids)[:, :-1]       # (B, L-1) 
 
# compute per-token rewards 
# per-token sampled KL penalty 
per_token_kl = ( 
    old_per_token_logps - reference_per_token_logps 
) * completion_mask 
 
per_token_rewards = -kl_beta * per_token_kl      # (B, L-1) 
 
# add outcome reward to final completion token 
positions = torch.arange( 
    completion_mask.size(1), 
    device=completion_mask.device, 
) 
last_token_idx = (completion_mask * positions).argmax(dim=-1) 
 
per_token_rewards[ 
    torch.arange(per_token_rewards.size(0)), 
    last_token_idx 
] += rewards 
 
# compute reward-to-go / return for each token 
returns = torch.flip( 
    torch.cumsum( 
        torch.flip(per_token_rewards, dims=[-1]), 
        dim=-1, 
    ), 
    dims=[-1], 
)                                                # (B, L-1) 
 
# compute token-level advantages 
advantages = returns - old_values                # (B, L-1) 
 
# perform multiple PPO updates over same rollout data 
for _ in range(num_ppo_epochs): 
 
    # policy forward pass 
    logits = policy(input_ids)                   # (B, L, V) 
    logits = logits[:, :-1, :]                   # (B, L-1, V) 
 
    # get logprobs of actions from current policy 
    per_token_logps = torch.log_softmax(logits, dim=-1) 
    per_token_logps = per_token_logps.gather( 
        dim=-1, 
        index=sampled_ids.unsqueeze(-1) 
    ).squeeze(-1)                                # (B, L-1) 
 
    # compute policy ratio 
    policy_ratio = torch.exp( 
        per_token_logps - old_per_token_logps 
    )                                            # (B, L-1) 
 
    # compute clipped policy ratio 
    clipped_policy_ratio = torch.clamp( 
        policy_ratio, 
        min=1.0 - ppo_eps, 
        max=1.0 + ppo_eps, 
    ) 
 
    # compute PPO clipped policy loss 
    policy_loss = -torch.min( 
        policy_ratio * advantages, 
        clipped_policy_ratio * advantages, 
    )                                            # (B, L-1) 
 
    # compute critic/value loss 
    values = critic(input_ids)[:, :-1]           # (B, L-1) 
    critic_loss = (values - returns) ** 2         # (B, L-1) 
 
    # aggregate losses across completion tokens 
    policy_loss = ( 
        (policy_loss * completion_mask).sum(dim=-1) 
        / completion_mask.sum(dim=-1) 
    ).mean() 
 
    critic_loss = ( 
        (critic_loss * completion_mask).sum(dim=-1) 
        / completion_mask.sum(dim=-1) 
    ).mean() 
 
    # combine policy + critic losses with critic weight 
    loss = policy_loss + critic_weight * critic_loss 
 
    # update policy and critic 
    optimizer.zero_grad() 
    loss.backward() 
    optimizer.step()

In agentic RL, the trajectory is a bit more complex than a single-step generation, as it does not merely contain LLM-generated tokens but also observations from the environment, e.g., retrieved content or the results of tool-call executions [7]. Basically, each rollout sequence is continuously parsed, and whenever a tool call (e.g a search tool) is produced by the model via special tokens, the training harness parses the tool call or tool calls, executes them, e.g., in a sandbox, and appends the resulting observations to the ongoing rollout sequence. This process continues iteratively until the maximum number of actions is reached or the model generates a final response (e.g. by producing answer special token). This is followed by reward modeling, which could use a rule-based reward system signaling whether the response is correct, or a more complex neural-based reward model, followed by updating the policy via policy-gradient methods, e.g. PPO. The policy-gradient objective is computed only over LLM-generated tokens, excluding non-LLM-generated tokens because they were not sampled from the policy, preventing interference from external content [7, 8]). However, these non-LLM-generated tokens are still part of the Transformer input, as the final answer and intermediate sub-trajectories attend to them.

PIC

Figure 1: [7]
2.6.14 Efficient Finetuning: LoRA &QLoRA

Given a language model \(p_{\theta }\), during full fine-tuning, the loss is the same as in SFT:

\[ \mathbf {L}(\theta ) = -\mathbb {E}_{(x,y)\in \mathbb {B}} \left [ \sum _{t=1}^{|y|} \log p_{\theta }(y_t \mid x,y_{<t}) \right ] \]

Each model parameter \(W\) is then updated using gradient descent:

\[ W \leftarrow W-\alpha \frac {\partial \mathbf {L}}{\partial W} \]

The problem is that

\[ \frac {\partial \mathbf {L}}{\partial W} \]

has the same size as \(W\), and all trainable model parameters need to be updated. Moreover, for each downstream task, full fine-tuning produces a different set of model parameters, which can be extremely large.

Instead of learning a different full \(W\) for each downstream task, LoRA or Low-rank Adapter assumes that the change in weights during model adaptation has a low intrinsic rank. It therefore learns the weight difference

\[ \Delta W = BA \]

such that

\[ W' = W + \Delta W = W + BA \]

where

\[ W\in \mathbb {R}^{d\times n}, \qquad B\in \mathbb {R}^{d\times r}, \qquad A\in \mathbb {R}^{r\times n} \]

with

\[ r \ll \min (d,n) \]

Only \(A\) and \(B\) are trainable; they are part of the computation graph, while \(W\) is frozen and treated as a constant. Each targeted linear projection therefore becomes

\[ y = Wx + BAx \]

instead of simply

\[ y=Wx \]

LoRA uses a random Gaussian initialization for \(A\) and zero initialization for \(B\). Formally, the fine-tuning objective can now be written as

\[ \mathbf {L}(\phi ) = -\mathbb {E}_{(x,y)\in \mathbb {B}} \left [ \sum _{t=1}^{|y|} \log p_{\theta ,\phi }(y_t\mid x,y_{<t}) \right ] \]

where \(\theta \) denotes the frozen pretrained model parameters and \(\phi \) denotes the set of all trainable LoRA parameters \(A\) and \(B\) added to the targeted layers. Thus,

\[ \theta =\text {constant}, \qquad \phi =\{A_\ell ,B_\ell \}_{\ell \in \mathbb {S}} \]

and optimization is performed only with respect to \(\phi \):

\[ \phi \leftarrow \phi -\alpha \frac {\partial \mathbf {L}}{\partial \phi }. \]

For each downstream task, only its corresponding \(\phi \) needs to be stored, while the same pretrained parameters \(\theta \) can be shared across tasks.

For an LLM, LoRA can be applied to selected linear projections in the attention and/or MLP modules. If \(L_{\mathrm {LoRA}}\) weight matrices of size \(d\times d\) are adapted with rank \(r\), the number of trainable LoRA parameters is

\[ |\phi | = 2L_{\mathrm {LoRA}}d \times r \]

since each adapted matrix introduces \( |A|+|B| = rd \times dr = 2d \times r \) trainable parameters.

Observe that, in LoRA, we increase the number of model parameters and thus the memory usage. However, during the backward pass, we do not need to compute or store

\[ \frac {\partial \mathbf {L}}{\partial W} \]

for the frozen weights. Instead, we only need to compute and store

\[ \frac {\partial \mathbf {L}}{\partial A} \qquad \text {and}\qquad \frac {\partial \mathbf {L}}{\partial B} \]

where \(A\) and \(B\) are much smaller tensors.

QLoRA is an extension of LoRA that augments the original linear projections in the same way as LoRA, while additionally quantizing the pretrained Transformer weights to 4-bit precision. Whenever a particular quantized weight or projection matrix is needed during the forward or backward pass, that tensor is temporarily dequantized to \(\mathrm {BF16}\) for the matrix multiplication. The temporary dequantized representation can then be released rather than kept permanently resident in memory, thereby reducing the memory required to fine-tune large models.

2.6.15 In-Context Learning

Language models, when made very large, tend to excel at new tasks unseen during pretraining. When the model is conditioned on a clear task description without any explicit examples (zero-shot learning), or on one or a few examples (few-shot learning), it seems to perform well, thereby avoiding the necessity of fine-tuning on each downstream task, which is prohibitively expensive. This is known as in-context learning, where the model, during inference, is able to generalize to new tasks conditioned on the context provided in the prompt.

2.6.16 Large Reasoning Models versus Vanilla LLMs

Vanilla LLMs are conditioned on the prompt to produce an answer \(y_{\mathrm {answer}}\), while reasoning models produces \( y = [y_{\mathrm {CoT}}, y_{\mathrm {answer}}]\), that is, a larger trajectory that includes a thinking or Chain-of-Thought (CoT) process \(y_{\mathrm {CoT}}\), typically enclosed inside special tokens to isolate the reasoning phase before actually producing the answer. Typically, the subtrajectory corresponding to the thought process, or the non-linear reasoning traces, is hidden from the user, and a summary may be shown instead.

Naturally, training a reasoning model can be very costly, as Supervised Fine-Tuning or SFT (where the goal is to align the model’s output distribution with the data distribution) may include CoT trajectories, a high-quality set of long reasoning traces, which can be synthetically generated by strong reasoning models or manually curated, in addition to non-reasoning datasets, before RL kicks in (this is what encourages the model to produce intermediate reasoning steps at inference time, which are referred to as thinking tokens).

Again, reasoning traces are separated from the generated answers via special tokens or delimiters. For example, a fine-tuning sample could look like

\[ \texttt {<think>} \]
\[ \ldots \]
\[ \texttt {</think>} \]
\[ \texttt {<answer>}\ldots \texttt {</answer>}.\]
2.6.17 Retrieval Augmented Generation (RAG)

Retrieval-Augmented Generation, or RAG, consists of combining information retrieval with the generative capabilities of LLMs to incorporate external knowledge (i.e., knowledge not internalized in the model parameters) and produce grounded responses (conditioned on the query and the retrieved knowledge), which can be represented as \(p_\theta (y \mid q, \operatorname {Retrieve}_k(q))\). The goal is mainly to mitigate hallucinations and build more reliable generative AI systems.

Conventional RAG operates as follows. Given a corpus of documents, each document is partitioned into chunks, and each chunk is projected into a neural embedding space. At query time, the query is also projected into the same embedding space, and the most relevant chunks are retrieved using approximate nearest-neighbor (ANN) search, optionally followed by reranking to refine the candidate selection. The retrieved chunks are then concatenated with the query to form an augmented prompt, which is provided to the LLM to generate a response.

2.6.18 Function Calling and MCP

LLMs produce a response conditioned on a given context or prompt. The response could use the internal knowledge of the language model (e.g., code generation) or answer something specific given in the prompt. In this case, the prompt could contain function-call descriptions (usually delimited using special control tokens), and the model could then produce tool or function calls according to the function schemas. This was already possible with in-context learning, but now LLMs are also trained on datasets that contain tool-use examples. The RL phase typically also teaches the model useful tool-use behaviors.

Once an LLM acquires tool-use capabilities, it can generate tool calls, and then the client or harness invoking the model reads the tool calls (what function to call and which arguments to pass to these functions), invokes them, potentially in parallel, and then injects the responses back into the context. When many LLM-based applications use the same APIs but integrate them differently, this creates fragmented integrations. Additionally, if the application cannot be modified to integrate the tools, e.g., Claude Code, then the available integrations are limited. This is why the Model Context Protocol (MCP) was introduced as a standard for tool integration, where first a standard client lists tools by communicating with an MCP server exposing the tools, and then each LLM-proposed tool call is mapped to an MCP remote call. Once the response is received, it is injected back into the context. Tool calls are what enable agents and multi-agent systems, as covered in the next section.

Finally, note that function calling opens the door to injection defects, where an adversary could redirect the model to produce malicious arguments. Thus, LLM output should be interpreted carefully (e.g., if you already know what the user ID or company ID is, form a source that you trust like jwt token, why read it again from the LLM?).

2.6.19 Multi-Agent Systems

Given a system prompt describing what the model should be and which persona it should assume, the model can emit a tool call. Using the response from the tool, we can then append the result to the context or conversation history. Each request to an LLM provider is typically represented as a series of input messages, with each message having a given role. These messages are then converted into a string using a chat template and subsequently converted into tokens. This constitutes one turn.

We can create a loop and continue this process until no more tool calls are produced (enabling the model to self-reflect and self-improvement). We call this an agent, and the loop is called the agent loop. There are many optimizations for managing the conversation state, including neural retrieval and history compression, in addition to safeguards and resume handling, but the basic idea is that an agent consists of a language model, a tool set, an execution runtime, and a loop.

Given one agent, we can create a multi-agent system where intelligence gets distributed across many specialized capable subagents. Depending on the pattern used, it could consist of a parent agent delegating tasks to subagents and then combining their results to produce a response. This is called the agent-as-a-tool pattern, where each agent interaction is wrapped as a tool that the parent agent can use. In this case, the parent and subagents may or may not share conversation history. Another pattern is a handoff, where the current agent delegates control to another agent, typically using a dedicated tool, while sharing the conversation history. Alternatively, one can use a routing agent that spawns or delegates to subagents, followed by a synthesizer that combines their results. Many other patterns are possible, and the appropriate architecture depends on the problem to be solved.

2.6.20 Language Models for Code

Similar to how text is represented as a sequence of tokens before being fed to an autoregressive model, source code is converted into a sequence of code tokens (e.g., variable names and function names) in the same way. The model can then be trained using the same cross-entropy loss, thereby enabling code completion at inference time. That is, given a contextual prompt, the model is used to predict the code or separate code snippets that the developer is likely to write and, if equipped to do so, can autonomously add and update files. Such models can be plugged into IDEs.