Weight Tying in Transformer Embeddings

Michael BrenndoerferUpdated June 17, 202553 min read

Part of Language AI Handbook

Explains how weight tying reduces transformer parameters by sharing the input embedding and output projection matrices.

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Weight Tying: Shared Embeddings in Transformers

Language models contain two large embedding matrices that seem to serve completely different purposes. One converts input tokens into continuous vectors at the start of the network. The other converts output vectors back into token probabilities at the end. If you examine these matrices for a model like GPT-2, you will find that each has 50,257 rows, one per vocabulary token, and 768 columns, one per hidden dimension. Together, they account for roughly 77 million parameters in a model whose total size is only about 117 million parameters. That means over 65 percent of all parameters in GPT-2 Small live in just these two matrices.

What makes this extraordinary is that both matrices answer fundamentally the same question: what does each vocabulary token mean within this model's learned representation space? The input embedding asks, "given this token, what vector should I pass into the transformer?" The output projection asks, "given this hidden state, how strongly does it correspond to each token?" These are two sides of the same coin. A model that has learned a rich representation for the word "justice" should encode it consistently whether reading it or generating it.

Weight tying exploits this insight with striking simplicity: instead of learning two separate matrices, the model uses one. The same embedding matrix that converts "justice" into a vector at the input also is the output target when the model wants to generate "justice". This single constraint halves the embedding-related parameters and, counter-intuitively, often improves model quality. You might expect that giving the model fewer degrees of freedom would hurt performance, but the opposite tends to be true. The constraint acts as a useful regularizer that forces coherent representations.

This chapter covers weight tying from its mathematical foundations to its practical implementation. We will understand exactly why tying makes theoretical sense, trace through numerical examples to see how shared embeddings work in practice, examine the gradient dynamics that make tied models learn differently, and explore encoder-decoder extensions where three matrices can be collapsed into one. We will also study the limitations: tying is not always appropriate, and knowing when to avoid it is as important as knowing how to implement it.

By the end of this chapter, you will understand one of the most universally adopted architectural choices in modern language modeling, appearing in GPT-2, BERT, T5, and nearly every transformer trained since 2017.

The Two Embedding Matrices

Every language model that processes text through a transformer architecture faces the same fundamental challenge at its boundaries. Transformers operate on continuous vectors, but text consists of discrete tokens. Bridging this gap requires learned mappings in both directions: from tokens to vectors at the input, and from vectors back to tokens at the output. These two mappings are implemented as the input embedding matrix and the output projection matrix, and understanding them precisely is the first step toward appreciating why tying them together makes sense.

Think of the transformer as a black box that maps sequences of dense vectors into new sequences of dense vectors. Before the transformer can do its work, you need to convert your token indices into vectors it can process. After the transformer produces its output, you need to convert those output vectors back into probabilities over the vocabulary. The two embedding matrices handle these conversion tasks.

Both matrices have the same shape, both operate over the same vocabulary, and both are involved at every single forward pass through the model. Yet in naive implementations, they are treated as entirely separate learned objects. Weight tying questions whether this separation is necessary or even desirable.

The Input Embedding Matrix

The input embedding matrix ERV×d\mathbf{E} \in \mathbb{R}^{V \times d} is one of the most fundamental components in any neural language model. It stores a learned dense vector representation for each token in the vocabulary. When the model sees token index tt, it retrieves the corresponding row from this matrix:

hin=E[t]\mathbf{h}_{\text{in}} = \mathbf{E}[t]

where:

  • E\mathbf{E}: the input embedding matrix of shape V×dV \times d, where every row contains the learned representation of one vocabulary token
  • tt: the input token index, an integer from 0 to V1V - 1
  • hin\mathbf{h}_{\text{in}}: the resulting embedding vector of dimension dd, which enters the transformer's processing pipeline
  • VV: the vocabulary size (50,257 for GPT-2, 32,000 for LLaMA-2, 128,000 for newer models)
  • dd: the embedding dimension, also called the model dimension or hidden dimension

This operation is purely a table lookup. Token 42 retrieves row 42 from the matrix. There is no computation beyond the indexing, which is one reason why embedding lookups are extremely fast in practice. The entire operation can be implemented as a gather operation on the weight matrix.

Over the course of training, the rows of E\mathbf{E} are shaped by gradient descent to capture the semantic and syntactic properties of each token. Tokens that appear in similar contexts will develop similar embeddings. Tokens that serve similar grammatical roles will develop representations that are geometrically related. The embedding matrix effectively becomes a learned dictionary that maps the discrete world of tokens to the continuous world of vector spaces.

The Output Projection Matrix

The output projection matrix WoutRV×d\mathbf{W}_{\text{out}} \in \mathbb{R}^{V \times d}, also called the language model head or LM head, performs the inverse operation. After the transformer has processed the input sequence and produced a final hidden state houtRd\mathbf{h}_{\text{out}} \in \mathbb{R}^d, the model needs to decide which token should come next. This decision is made by computing a score for every token in the vocabulary:

z=Wouthout\mathbf{z} = \mathbf{W}_{\text{out}} \mathbf{h}_{\text{out}}

where:

  • z\mathbf{z}: the output logits vector of length VV, one unnormalized score per vocabulary token
  • Wout\mathbf{W}_{\text{out}}: the output projection matrix of shape V×dV \times d, where each row encodes the "target pattern" for one vocabulary token
  • hout\mathbf{h}_{\text{out}}: the final hidden state from the last transformer layer, a vector of dimension dd

Each row of Wout\mathbf{W}_{\text{out}} contains what you might call the "output embedding" for one vocabulary token. The full matrix multiplication computes the dot product between the hidden state and each token's output embedding in one vectorized operation, yielding a score for every token in the vocabulary simultaneously.

After computing these logits, we apply the softmax function to convert them into a proper probability distribution over the vocabulary for the next token prediction:

P(tokenicontext)=exp(zi)k=1Vexp(zk)P(\text{token}_i \mid \text{context}) = \frac{\exp(z_i)}{\sum_{k=1}^{V} \exp(z_k)}

where:

  • P(tokenicontext)P(\text{token}_i \mid \text{context}): the probability of token ii being the correct next token given the context
  • ziz_i: the logit for token ii, computed as wihout\mathbf{w}_i \cdot \mathbf{h}_{\text{out}} where wi\mathbf{w}_i is row ii of Wout\mathbf{W}_{\text{out}}
  • exp(zi)\exp(z_i): the exponential applied to logit ii, which ensures all values are positive
  • k=1Vexp(zk)\sum_{k=1}^{V} \exp(z_k): the sum of all exponentials across the full vocabulary, serving as a normalizing constant

Why does this formula make sense? Notice that taking the exponential of each logit guarantees positive values regardless of the sign of the original logit, which is a prerequisite for valid probabilities. The denominator then ensures all values sum to exactly 1. Tokens with higher logits, those whose output embeddings more closely align with the hidden state, receive higher probabilities.

The Critical Observation: Identical Shapes

Notice the dimensional symmetry: both E\mathbf{E} and Wout\mathbf{W}_{\text{out}} have exactly the same shape, V×dV \times d. Both have VV rows, one per vocabulary token. Both have dd columns, one per hidden dimension. Both map between the same two spaces: the discrete token space with VV elements and the continuous embedding space with dd dimensions.

This symmetry is not a coincidence. It reflects the deeper fact that both matrices perform the same abstract function: they define a correspondence between tokens and points in a dd-dimensional vector space. The input embedding says "token tt lives at point et\mathbf{e}_t." The output projection says "a hidden state should produce token tt with probability proportional to how much it resembles wt\mathbf{w}_t." These two statements describe the same geometric relationship from opposite directions.

In[3]:
Code
import torch.nn as nn

# Typical model dimensions
vocab_size = 50000
hidden_dim = 768

# Without weight tying: two separate matrices
input_embedding = nn.Embedding(vocab_size, hidden_dim)
output_projection = nn.Linear(hidden_dim, vocab_size, bias=False)

# Count parameters
input_params = vocab_size * hidden_dim
output_params = hidden_dim * vocab_size
total_untied = input_params + output_params
Out[4]:
Console
Parameter count without weight tying:
  Input embedding:      38,400,000 parameters
  Output projection:    38,400,000 parameters
  Total:                76,800,000 parameters

Both matrices have identical size: 50,000 x 768
That's 76.8M parameters just for the two embedding matrices.

With a vocabulary of 50,000 tokens and hidden dimension of 768, each matrix contains over 38 million parameters. Together, they account for nearly 77 million parameters. For a model like GPT-2 Small, which has about 117 million total parameters, this means over 65 percent of the model's capacity lives in these two identically sized matrices. The question weight tying asks is: do we really need both?

The Weight Tying Idea

We have established that language models maintain two large matrices with identical shapes: one for reading tokens and one for generating them. Both have dimensions V×dV \times d, where each row represents one vocabulary token. This structural symmetry hints at a deeper connection worth pursuing carefully.

Think about what information these matrices encode. The input embedding learns "what does this token mean when I read it?" The output projection learns "what hidden state pattern should produce this token?" But at a deeper level, these are really two perspectives on the same question: what is the semantic identity of this token within the model's learned representation space? A token's meaning should not depend on whether you are reading it or generating it.

Think of it this way. If you learned the word "melancholy" in school, you would use the same mental concept whether you are recognizing that word in a sentence you are reading or deciding to write it in a sentence you are composing. You do not maintain one neural representation of "melancholy" for reading and a completely separate representation for writing. You have one underlying concept, and it serves both purposes. Weight tying imposes the same discipline on language models.

a model's representation of any token should be internally consistent: the vector that captures "what this token means as input" should be the same vector that is "the target to match when generating this token as output." Any discrepancy between these two representations is wasted degrees of freedom at best, and incoherence at worst.

The Core Formula

Weight tying formalizes this intuition by collapsing both matrices into one:

Wout=E\mathbf{W}_{\text{out}} = \mathbf{E}

where:

  • Wout\mathbf{W}_{\text{out}}: the output projection matrix, now set equal to E\mathbf{E} rather than being an independent learned parameter
  • E\mathbf{E}: the input embedding matrix of shape V×dV \times d, which now serves both roles simultaneously

This single equation halves the embedding-related parameters. But what does it mean computationally? Let's trace through the math to understand how the shared matrix serves both roles in a coherent way.

From Intuition to Formula

When a token enters the model, we look up its embedding. This operation is unchanged by tying:

hin=E[t]\mathbf{h}_{\text{in}} = \mathbf{E}[t]

The token at index tt retrieves row tt from the matrix, a simple table lookup. Training updates this row to encode useful information about token tt for downstream processing.

When the transformer produces its final hidden state hout\mathbf{h}_{\text{out}}, we convert this dd-dimensional vector into scores for all VV vocabulary tokens. With weight tying, we use the same matrix for this conversion:

z=Ehout\mathbf{z} = \mathbf{E} \cdot \mathbf{h}_{\text{out}}

This matrix multiplication produces a vector of VV logits. But what is happening geometrically inside this operation? Each logit is a dot product between the hidden state and one row of the embedding matrix.

The Dot Product as Similarity

The logit for token ii decomposes as follows. We need a formula that takes the hidden state hout\mathbf{h}_{\text{out}} and produces a score reflecting how likely token ii is to be the correct next word. With tied weights, we compute:

zi=eihout=j=1dei,jhjz_i = \mathbf{e}_i \cdot \mathbf{h}_{\text{out}} = \sum_{j=1}^{d} e_{i,j} \cdot h_j

where:

  • ziz_i: the logit (unnormalized score) for token ii, a single real number
  • ei\mathbf{e}_i: the embedding vector for token ii (row ii of E\mathbf{E}), a dd-dimensional vector with components ei,1,ei,2,,ei,de_{i,1}, e_{i,2}, \ldots, e_{i,d}
  • hout\mathbf{h}_{\text{out}}: the final hidden state vector, with dd components h1,h2,,hdh_1, h_2, \ldots, h_d
  • dd: the embedding dimension

Why does this formula make sense? Notice that the dot product measures geometric alignment between two vectors. Two vectors pointing in the same direction in the dd-dimensional embedding space yield a large positive value. Orthogonal vectors yield zero. Vectors pointing in opposite directions yield a large negative value. Softmax then converts these alignment scores into probabilities, so the token whose embedding most closely aligns with the hidden state receives the highest probability.

This creates a beautiful geometric interpretation: the transformer's job is to produce a hidden state that points in the direction of the correct next token's embedding. When processing the context "The cat sat on the ___", the transformer should generate a hidden state that is close to the embedding vectors for words like "mat," "floor," or "couch," and far from the embedding vectors for words like "quantum" or "bureaucracy."

With tied weights, the embedding vectors that serve as targets for this similarity search are exactly the same vectors used to represent tokens in the input. The model must learn one coherent embedding for each token that works both as an input representation and as an output target. This constraint is not a restriction; it is a clarification that forces the model to build consistent semantics.

Implementation Note

In PyTorch, the embedding matrix has shape (V,d)(V, d) and hidden states typically have shape (B,S,d)(B, S, d) where BB is the batch size and SS is the sequence length. To compute logits for a full batch of hidden states, we use hidden_states @ embedding.weight.T, which performs a batched matrix multiplication. The result has shape (B,S,V)(B, S, V), giving one logit per token per position. This single operation replaces what would otherwise require a separate nn.Linear(hidden_dim, vocab_size, bias=False) layer.

Implementation

Translating this math into code is straightforward. We create a single embedding matrix and use it for both encoding (lookup) and decoding (matrix multiplication):

In[5]:
Code
import torch
import torch.nn as nn


class TiedEmbeddings(nn.Module):
    """Language model head with tied input and output embeddings."""

    def __init__(self, vocab_size, hidden_dim):
        super().__init__()
        # Single shared embedding matrix
        self.embedding = nn.Embedding(vocab_size, hidden_dim)

    def encode(self, token_ids):
        """Convert token IDs to embeddings."""
        return self.embedding(token_ids)

    def decode(self, hidden_states):
        """Project hidden states to vocabulary logits."""
        # Use embedding weight matrix transposed
        return torch.matmul(hidden_states, self.embedding.weight.T)


# Create tied embeddings
tied_model = TiedEmbeddings(vocab_size, hidden_dim)
tied_params = vocab_size * hidden_dim
Out[6]:
Console
Parameter count with weight tying:
  Shared embedding:     38,400,000 parameters

Parameter reduction: 38,400,000 parameters (50% savings)
We eliminated one full 50,000 x 768 matrix (38.4M parameters).

The parameter savings are substantial. We eliminated one entire V×dV \times d matrix, cutting embedding-related parameters in half. For models with large vocabularies or smaller hidden dimensions, this reduction represents a significant fraction of total model size. More importantly, as we will see shortly, this reduction often comes with improved performance, not degraded performance.

Why Weight Tying Makes Sense

Weight tying reduces memory use, but its theoretical justification also explains why input and output embeddings should be related. Understanding this justification deepens your intuition for how transformers represent language.

Consider what each embedding encodes over the course of training. The input embedding for token tt gets updated whenever token tt appears in the input, with gradients flowing backward from the loss through all the transformer layers above. It learns a representation that makes downstream processing easier: similar tokens develop similar embeddings, tokens that play similar grammatical roles cluster together, and the geometric structure of the embedding space reflects the distributional statistics of the language.

The output embedding for token tt gets updated whenever token tt should be predicted as the next word, with gradients flowing directly from the cross-entropy loss. It learns what kind of hidden state the transformer should produce when "token tt is the right answer." The hidden state that generates "cat" should look similar to the output embedding for "cat."

Both representations capture the same fundamental thing: the contextual signature of token tt within the model's learned world model. A token's input representation and its output representation are both shaped by the same distributional statistics of the language. They are really two approximations to the same underlying concept, learned through slightly different optimization paths.

Distributional Hypothesis Connection

Weight tying has a deep connection to the distributional hypothesis in linguistics: "words that occur in similar contexts have similar meanings." The input embedding captures what contexts a word tends to appear in. The output embedding captures what words appear in what contexts. These are two sides of the same distributional coin. Tying them together embeds the distributional hypothesis directly into the model's architecture.

The Consistency Argument

Here is a simple argument for why tying should help. Suppose you have two separate matrices, E\mathbf{E} and Wout\mathbf{W}_{\text{out}}, and the model learns them independently. After training, you look at the embedding for "dog" in the input matrix and the embedding for "dog" in the output matrix. If the model has learned well, these two vectors should be similar, because both capture the meaning of "dog." But they are not required to be similar, so the model wastes capacity representing the same concept twice in slightly different ways.

Weight tying removes this redundancy by decree. The model is forced to find a single representation that works for both purposes, which means every gradient update contributes to a more coherent and consistent token representation. You are effectively doubling the amount of learning signal that shapes each token's embedding, because both the input path and the output path update the same weights.

This consistency argument applies directly to language modeling, where the input and output vocabularies are identical. The model reads and writes in the same language, using the same tokens. There is no fundamental reason why the concept of "dog" should be represented differently depending on whether the model is reading it or generating it.

Empirical Evidence

Research has consistently confirmed that weight tying helps rather than hurts performance:

  • Press and Wolf (2017) introduced the technique systematically and demonstrated that tying input and output embeddings improves perplexity across language modeling benchmarks, despite having fewer parameters. This was the result that established weight tying as a standard practice.

  • Inan et al. (2017) provided complementary theoretical analysis, connecting weight tying to a regularization perspective where the shared matrix is encouraged to satisfy both input and output objectives simultaneously.

  • Practical adoption: Most modern language models, including GPT-2, BERT, RoBERTa, T5, and their descendants, use weight tying by default. The technique has become so standard that its use is often not even mentioned in architecture descriptions.

The improvement in perplexity also reflects more than the regularization from having fewer parameters. Tied embeddings tend to learn better representations because gradients from the output loss flow directly into the input embeddings, and gradients from the input embedding usage flow into the output predictions. The two optimization objectives reinforce each other.

Implementation Details

Let's implement a complete language model head with weight tying, handling the practical details that matter in real systems. A production-grade implementation needs to manage positional encodings, dropout, and the subtleties of the embedding scale.

The key engineering insight in a full implementation is that the embedding matrix is shared by reference, not by copying. Both the encode and decode operations point to the same underlying tensor. When gradients update the shared matrix, they simultaneously improve both the input representations and the output targets. This is not a coincidence or a side effect. It is the core mechanism that makes weight tying beneficial.

In[7]:
Code
class LanguageModelHead(nn.Module):
    """
    Complete language model embedding layer with weight tying.

    Handles token embedding, positional encoding, and output projection
    using a single shared vocabulary embedding matrix.
    """

    def __init__(
        self, vocab_size, hidden_dim, max_seq_len, dropout=0.1, tie_weights=True
    ):
        super().__init__()
        self.hidden_dim = hidden_dim
        self.tie_weights = tie_weights

        # Token embeddings (always needed)
        self.token_embedding = nn.Embedding(vocab_size, hidden_dim)

        # Position embeddings (not tied)
        self.position_embedding = nn.Embedding(max_seq_len, hidden_dim)

        # Output projection: tied or separate
        if tie_weights:
            self.output_projection = None  # Will use token_embedding.weight
        else:
            self.output_projection = nn.Linear(
                hidden_dim, vocab_size, bias=False
            )

        self.dropout = nn.Dropout(dropout)

        # Initialize embeddings
        self._init_weights()

    def _init_weights(self):
        """Initialize embedding weights."""
        nn.init.normal_(self.token_embedding.weight, mean=0.0, std=0.02)
        nn.init.normal_(self.position_embedding.weight, mean=0.0, std=0.02)
        if self.output_projection is not None:
            nn.init.normal_(self.output_projection.weight, mean=0.0, std=0.02)

    def embed(self, token_ids):
        """Convert token IDs to embeddings with position information."""
        batch_size, seq_len = token_ids.shape

        # Token embeddings
        tok_emb = self.token_embedding(token_ids)

        # Position embeddings
        positions = torch.arange(seq_len, device=token_ids.device)
        pos_emb = self.position_embedding(positions)

        # Combine and apply dropout
        return self.dropout(tok_emb + pos_emb)

    def project(self, hidden_states):
        """Project hidden states to vocabulary logits."""
        if self.tie_weights:
            # Use shared embedding matrix
            return torch.matmul(hidden_states, self.token_embedding.weight.T)
        else:
            return self.output_projection(hidden_states)


# Compare tied vs untied
model_tied = LanguageModelHead(
    vocab_size, hidden_dim, max_seq_len=512, tie_weights=True
)
model_untied = LanguageModelHead(
    vocab_size, hidden_dim, max_seq_len=512, tie_weights=False
)


def count_parameters(model):
    return sum(p.numel() for p in model.parameters())
Out[8]:
Console
Parameter comparison:
  Tied weights:     38,793,216 parameters
  Untied weights:   77,193,216 parameters
  Difference:       38,400,000 parameters

The tied model saves 49.7% of embedding-related parameters

The key implementation detail is in the project method. With tied weights, we directly use self.token_embedding.weight.T instead of a separate projection layer. This ensures that gradients flow through the same parameters during both forward and backward passes. There is no additional memory allocated for a second embedding matrix. The model literally shares the same tensor object.

Notice that position embeddings are not tied and do not participate in the output projection. This is by design. Position embeddings encode sequential position information that has no meaning in the output vocabulary space. Only the token embeddings, which capture the semantic identity of each vocabulary item, are shared between input and output.

Verifying the Tying

Let's verify that our implementation shares parameters correctly and that gradients flow as expected:

In[9]:
Code
# Test the tied model
test_tokens = torch.randint(0, vocab_size, (2, 10))  # Batch of 2, length 10

# Get embeddings
embeddings = model_tied.embed(test_tokens)

# Simulate transformer processing (just use embeddings as output for demo)
hidden_output = embeddings

# Get logits
logits = model_tied.project(hidden_output)

# Verify gradient flow
loss = logits.sum()
loss.backward()
Out[10]:
Console
Input shape: torch.Size([2, 10])
Embedding shape: torch.Size([2, 10, 768])
Output logits shape: torch.Size([2, 10, 50000])

Token embedding gradient shape: torch.Size([50000, 768])
Gradient flows through shared weights: True
Gradient norm on shared embedding: 1143.0863

Gradients arrive from both input and output paths simultaneously.

The gradient from the output loss flows directly into the token embedding matrix, confirming that the tying works correctly. With tied weights, every backward pass accumulates gradients from multiple sources into the same underlying parameters. This is the mechanism by which tied models receive more learning signal per parameter.

A Worked Example

The formulas above describe weight tying abstractly, but seeing the mechanism in action makes it concrete. Let's build a tiny language model and trace through exactly how tied embeddings convert a hidden state into token probabilities. We will work with small enough numbers to inspect every value manually.

Setting Up a Toy Vocabulary

We will work with a vocabulary of just seven words using 4-dimensional embeddings. These small numbers let us inspect every value and understand exactly what is happening at each step. In a real model you might have 50,000 words and 768 dimensions, making manual inspection impractical, but the same mathematical operations apply at every scale.

In[11]:
Code
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F

# Create a tiny vocabulary for illustration
tiny_vocab = ["the", "cat", "sat", "on", "mat", "dog", "ran"]
tiny_size = len(tiny_vocab)
tiny_dim = 4

# Initialize with small, interpretable embeddings


# Create tied embedding model
class TinyLM(nn.Module):
    def __init__(self):
        super().__init__()
        self.embedding = nn.Embedding(tiny_size, tiny_dim)
        # Initialize with small random values
        nn.init.uniform_(self.embedding.weight, -0.5, 0.5)

    def forward(self, hidden_state):
        """Get probabilities for next token given hidden state."""
        logits = torch.matmul(hidden_state, self.embedding.weight.T)
        return F.softmax(logits, dim=-1)


model = TinyLM()
Out[12]:
Console
Vocabulary: ['the', 'cat', 'sat', 'on', 'mat', 'dog', 'ran']

Embedding matrix (7 tokens x 4 dims):
---------------------------------------------
  the   : [-0.366, -0.499, +0.362, -0.212]
  cat   : [-0.203, -0.113, -0.029, -0.206]
  sat   : [+0.114, -0.049, -0.306, -0.257]
  on    : [+0.264, +0.247, -0.104, +0.128]
  mat   : [+0.276, -0.166, +0.333, -0.484]
  dog   : [-0.399, -0.407, +0.308, +0.145]
  ran   : [-0.390, -0.450, -0.434, +0.312]

Each word has a 4-dimensional embedding vector. These are randomly initialized, as they would be before training. In a trained model, semantically similar words would have similar embeddings. The words "cat" and "dog" would cluster together because they are both animals that appear in similar grammatical contexts. The words "sat" and "ran" would form another cluster as verbs. Weight tying means that these same clusters serve as targets for the output prediction: when the model wants to generate "cat," it aims to produce a hidden state that points in the direction of the "cat" embedding vector.

The Prediction Mechanism

Now comes the key insight. Suppose the transformer has processed some context and produced a final hidden state. With weight tying, we predict the next token by computing dot products between this hidden state and every embedding in our vocabulary.

Let's simulate this. We'll create a hidden state that is similar to the "cat" embedding, as if the model were about to predict "cat" as the next word. This simulates what would happen if the transformer correctly learned to associate some context with the concept of "cat":

In[13]:
Code
# Suppose the transformer outputs a hidden state similar to "cat"'s embedding
cat_embedding = model.embedding.weight[1].detach()  # Index 1 = "cat"

# Move from the other-token centroid toward "cat" so the intended token wins.
# This fixed construction keeps every render variant identical without reseeding.
other_embeddings = torch.cat(
    [model.embedding.weight[:1], model.embedding.weight[2:]], dim=0
).detach()
other_centroid = other_embeddings.mean(dim=0)
hidden_state = cat_embedding + 2.0 * (cat_embedding - other_centroid)
hidden_state = hidden_state.unsqueeze(0)  # Add batch dimension

# Get next token probabilities
probs = model(hidden_state).squeeze()
assert probs.argmax().item() == 1, (
    "The pedagogical hidden state must predict 'cat'."
)

The hidden state is intentionally constructed to align with "cat"'s embedding while moving away from the average of the other token embeddings. In a real model, the transformer layers would produce this hidden state based on the input context. The contrastive offset represents the fact that the model's output is not a perfect copy of any single embedding; it is a point in the embedding space that aligns more strongly with some tokens than others.

Tracing the Dot Products Step by Step

Now let's examine what happens inside the forward pass. For each vocabulary token, we compute the dot product between the hidden state and that token's embedding. This gives us a raw score, which softmax then converts into a probability:

Out[14]:
Console
Hidden state (similar to 'cat' embedding):
  [-0.4412608   0.10209379 -0.14097796 -0.49593773]

Dot products with each token embedding:
--------------------------------------------------
  the   : dot = +0.1648  =>  P = 0.1576
  cat   : dot = +0.1844  =>  P = 0.1607
  sat   : dot = +0.1155  =>  P = 0.1500
  on    : dot = -0.1397  =>  P = 0.1162
  mat   : dot = +0.0545  =>  P = 0.1411
  dog   : dot = +0.0190  =>  P = 0.1362
  ran   : dot = +0.0325  =>  P = 0.1381

Most likely next token: 'cat'
Probability assigned:   0.1607

All probabilities sum to: 1.000000

The dot product reveals the alignment between the hidden state and each embedding. "Cat" receives the highest score because its embedding is most similar to the hidden state we constructed. After softmax normalization, this translates to the highest probability. The other tokens receive lower but non-zero probabilities. This reflects the fact that uncertainty exists about the correct next token.

This is the heart of weight tying: the same embedding that represents "cat" for input processing is also the target pattern the model aims to produce when generating "cat". The transformer's job is to transform the input context into a hidden state that matches the correct next token's embedding. Learning happens both by improving the embedding representations and by improving the transformer's ability to produce hidden states that match those representations.

Numerical Walkthrough of Softmax

Let us trace the complete computation from logits to probabilities for the first three tokens to make the math fully concrete. Suppose the hidden state produces logits of z=[0.8,1.4,0.3,0.1,0.5,0.2,0.4]z = [0.8, 1.4, 0.3, 0.1, 0.5, 0.2, 0.4] for our seven tokens. The softmax computation proceeds:

exp(zthe)=exp(0.8)2.226exp(zcat)=exp(1.4)4.055exp(zsat)=exp(0.3)1.350\begin{aligned} \exp(z_{\text{the}}) &= \exp(0.8) \approx 2.226 \\ \exp(z_{\text{cat}}) &= \exp(1.4) \approx 4.055 \\ \exp(z_{\text{sat}}) &= \exp(0.3) \approx 1.350 \\ &\vdots \end{aligned}

The normalizing denominator sums all seven exponentials, and each token's probability is its exponential divided by this sum. The token with the highest logit, "cat" in this example, receives the highest probability. The key observation is that with tied weights, these logit values are dot products with the input embedding vectors, so improving the input embedding for "cat" simultaneously makes it easier to predict "cat" as an output.

Visualizing Embedding Similarity

To better understand how the dot product works as a similarity measure, let's visualize the pairwise similarities between all embeddings in our tiny vocabulary along with the hidden state:

Out[15]:
Visualization
Heatmap showing dot product similarities between seven token embeddings and a hidden state vector.
Pairwise dot product similarities between token embeddings and the hidden state. The hidden state row shows highest similarity with 'cat', which is expected since we constructed it to be close to the cat embedding. The diagonal tends toward high values because each embedding is maximally aligned with itself. With tied weights, the same geometric structure that determines input representations also determines output predictions.

The heatmap reveals the geometric structure of our embedding space. The bottom row and rightmost column show how similar the hidden state is to each vocabulary embedding. Since we constructed the hidden state to resemble "cat," that cell shows the highest similarity in the hidden row. The diagonal entries reflect each token's self-similarity. Off-diagonal entries show how similar different tokens are to each other in the learned representation space.

With tied weights, this same similarity matrix governs both how tokens are processed as input and how they are selected as output. Making "cat" more recognizable as an input (by moving its embedding to a more distinctive location in the space) simultaneously makes it easier to generate "cat" as an output (by making the target point more distinct from other tokens).

Scaling Considerations

Weight tying becomes more or less impactful depending on how large the vocabulary is relative to the rest of the model. In a tiny model, the embedding matrices might account for the majority of parameters. In a massive model with many deep transformer layers, the embedding fraction shrinks. Understanding this scaling relationship helps you calibrate how much you should care about tying in different contexts.

The fundamental relationship is simple: the number of embedding parameters grows linearly with vocabulary size and embedding dimension, while the number of transformer layer parameters grows quadratically with the hidden dimension (due to the 4d24d^2 parameters in attention and feedforward components). As models scale up in depth, the relative importance of the embeddings decreases.

In[16]:
Code
def analyze_weight_tying_impact(
    vocab_size, hidden_dim, num_layers, d_ff_multiplier=4
):
    """Calculate what fraction of parameters weight tying saves."""

    # Embedding parameters
    embedding_params = vocab_size * hidden_dim
    tied_savings = embedding_params  # One matrix instead of two

    # Per-layer transformer parameters (approximate)
    attention_params = 4 * hidden_dim * hidden_dim  # Q, K, V, O projections
    ffn_params = 2 * hidden_dim * (hidden_dim * d_ff_multiplier)  # Up and down
    layer_norm_params = 4 * hidden_dim  # Two layer norms
    layer_params = attention_params + ffn_params + layer_norm_params

    # Total model parameters
    total_layers = num_layers * layer_params
    total_untied = (
        2 * embedding_params + total_layers
    )  # Input + output embeddings
    total_tied = embedding_params + total_layers  # Shared embedding

    return {
        "embedding_params": embedding_params,
        "layer_params": total_layers,
        "total_untied": total_untied,
        "total_tied": total_tied,
        "savings": total_untied - total_tied,
        "savings_pct": 100 * (total_untied - total_tied) / total_untied,
    }


# Analyze different model configurations
configs = [
    ("Small (GPT-2)", 50257, 768, 12),
    ("Medium", 50257, 1024, 24),
    ("Large", 50257, 1280, 36),
    ("XL", 50257, 1600, 48),
    ("Large vocab", 128000, 1024, 24),  # Like newer models
]
Out[17]:
Console
Weight Tying Impact Analysis
=====================================================================================
Config                    Vocab   Hidden  Layers         Savings % of Model
-------------------------------------------------------------------------------------
Small (GPT-2)            50,257      768      12      38,597,376      23.8%
Medium                   50,257     1024      24      51,463,168      12.7%
Large                    50,257     1280      36      64,328,960       7.7%
XL                       50,257     1600      48      80,411,200       4.9%
Large vocab             128,000     1024      24     131,072,000      23.2%

For smaller models, weight tying can save 10 to 20 percent of total parameters. As models grow deeper, the relative savings decrease because transformer layers dominate, but the absolute parameter savings remain substantial. Even in a very large model, eliminating 38 million or 131 million parameters is meaningful from both a memory and computational perspective.

The most striking case in the table is the large vocabulary configuration. Models with 128,000-token vocabularies, used by newer multilingual models and those covering many programming languages, see their embedding fraction rise even at larger hidden dimensions. This means weight tying becomes relatively more valuable as vocabulary size increases, a trend that will continue as models adopt even larger vocabularies.

To understand why weight tying matters more for some models than others, let's visualize how parameters are distributed:

Out[18]:
Visualization
Stacked bar chart showing parameter distribution across embedding and transformer layers for five model configurations.
Parameter distribution in different model configurations. Smaller models allocate a larger fraction of their parameters to embeddings, making weight tying more impactful. Larger and deeper models are dominated by transformer layer parameters, reducing the relative benefit. The large vocabulary model stands out as an exception where embedding costs remain high despite substantial depth.

The visualization makes the trade-off clear. In GPT-2 Small, embeddings consume over 15 percent of parameters, so weight tying provides substantial savings. In the XL configuration, embeddings are less than 5 percent of the model. The "Large vocab" case is instructive: despite having more layers, the 128K vocabulary pushes embedding costs back up, reminding us that vocabulary size is a critical variable in this calculation.

Out[19]:
Visualization
Bar chart showing absolute parameter savings in millions for five model configurations.
Absolute parameter savings from weight tying across different model scales. Larger vocabularies produce more savings in absolute terms, with the large-vocab model saving over 130 million parameters.
Bar chart showing percentage parameter savings for five model configurations.
Relative parameter savings as a percentage of total model parameters. Weight tying provides larger relative benefits for smaller models where embeddings constitute a greater fraction of total capacity.

The "Large vocab" configuration shows an interesting pattern: models with bigger vocabularies, like those using 128K-token vocabularies for multilingual support or code generation, benefit more from weight tying because the embedding matrix is a larger fraction of total parameters. As newer models adopt increasingly large vocabularies to support diverse scripts and domains, weight tying becomes progressively more important as an efficiency measure.

Encoder-Decoder Weight Tying

So far we have discussed tying input and output embeddings within a single decoder-only model. Encoder-decoder architectures offer additional tying opportunities that can eliminate even more redundant parameters. These architectures, which underpin models like T5, BART, and the original transformer, have three embedding matrices where a decoder-only model has two.

In a sequence-to-sequence model, you have three distinct embedding operations: the encoder processes source tokens through its own embedding layer, the decoder processes target tokens through its own embedding layer during training (teacher forcing), and the decoder produces output logits through the output projection. All three matrices have the same shape (V×d)(V \times d) if the model uses a shared source-target vocabulary, which is common for tasks like machine translation where modern BPE tokenizers can cover both languages.

The question is whether to tie any or all of these three matrices. Research has explored several configurations:

  • Decoder-only tying: Tie decoder input and output, leave encoder separate
  • Full tying: Tie all three together with a single shared matrix
  • No tying: Keep all three separate

Research has shown that full tying works well in many settings:

Eenc=Edec=Wout\mathbf{E}_{\text{enc}} = \mathbf{E}_{\text{dec}} = \mathbf{W}_{\text{out}}

where:

  • Eenc\mathbf{E}_{\text{enc}}: the encoder input embedding matrix of shape V×dV \times d, used when the encoder reads the source sequence
  • Edec\mathbf{E}_{\text{dec}}: the decoder input embedding matrix of shape V×dV \times d, used when the decoder reads target tokens during teacher forcing
  • Wout\mathbf{W}_{\text{out}}: the decoder output projection matrix of shape V×dV \times d, used when the decoder generates output logits
  • VV: the shared vocabulary size
  • dd: the hidden dimension

This three-way tying means a single learned embedding serves all three roles: encoding source tokens, encoding target tokens during teacher forcing, and defining the output distribution over the vocabulary. The parameter savings are now three-fold compared to having no tying at all.

The theoretical justification extends naturally. If source and target sequences are in the same language or share significant vocabulary (as they do in paraphrase generation, summarization, or code refinement tasks), then a token should have a consistent representation regardless of whether it appears in the input or output. Even in cross-lingual tasks, if the source and target languages share a BPE vocabulary with many overlapping subword units, some degree of tying can still be beneficial.

In[20]:
Code
class EncoderDecoderEmbeddings(nn.Module):
    """
    Shared embeddings for encoder-decoder architecture.

    All three embedding matrices are tied together.
    """

    def __init__(self, vocab_size, hidden_dim, tie_all=True):
        super().__init__()
        self.tie_all = tie_all

        # Shared embedding (used by all three roles)
        self.shared_embedding = nn.Embedding(vocab_size, hidden_dim)

        if not tie_all:
            # Separate embeddings if not tying
            self.encoder_embedding = nn.Embedding(vocab_size, hidden_dim)
            self.decoder_embedding = nn.Embedding(vocab_size, hidden_dim)
            self.output_projection = nn.Linear(
                hidden_dim, vocab_size, bias=False
            )

    def encode_input(self, token_ids):
        """Embed encoder input tokens."""
        if self.tie_all:
            return self.shared_embedding(token_ids)
        return self.encoder_embedding(token_ids)

    def decode_input(self, token_ids):
        """Embed decoder input tokens."""
        if self.tie_all:
            return self.shared_embedding(token_ids)
        return self.decoder_embedding(token_ids)

    def project_output(self, hidden_states):
        """Project decoder hidden states to vocabulary."""
        if self.tie_all:
            return torch.matmul(hidden_states, self.shared_embedding.weight.T)
        return self.output_projection(hidden_states)


# Compare parameter counts
enc_dec_tied = EncoderDecoderEmbeddings(vocab_size, hidden_dim, tie_all=True)
enc_dec_untied = EncoderDecoderEmbeddings(vocab_size, hidden_dim, tie_all=False)
Out[21]:
Console
Encoder-Decoder Embedding Parameters:
  Fully tied:     38,400,000 (1 matrix)
  Untied:        153,600,000 (3 matrices)
  Savings:       115,200,000 (75%)

Full tying eliminates 115.2M parameters from the embedding layers alone.

For encoder-decoder models, full weight tying eliminates two-thirds of embedding parameters. T5, one of the most successful encoder-decoder transformers, uses this three-way tying by default. The original paper reported that tying all three matrices worked better than partial tying or no tying for the text-to-text tasks T5 was designed for.

The project_output method still uses torch.matmul(hidden_states, self.shared_embedding.weight.T) even though the "decoder" might have a different hidden dimension scale than the encoder. In practice, T5 uses the same hidden dimension for encoder and decoder, so the shared matrix works without an additional projection. If the encoder and decoder had different hidden dimensions, full tying would not be possible without additional projections.

Effects on Training Dynamics

Weight tying does not just reduce parameters. It changes the fundamental dynamics of how the model learns, and understanding these dynamics helps explain why tied models often outperform untied models with the same number of free parameters.

The most important change is in how gradients flow. With separate embedding matrices, the input embedding receives gradients only through the transformer layers, while the output projection receives gradients directly from the cross-entropy loss. With tied weights, the single shared matrix receives gradients from both sources simultaneously. This is a form of implicit multi-task learning: the embedding must simultaneously satisfy two objectives, and each objective provides additional signal about what good representations look like.

Consider a rare token like a technical term that appears infrequently in the training corpus. With separate embeddings, the input embedding for this token receives sparse gradient updates (only when it appears in the input), and the output embedding receives sparse updates (only when it should be predicted). With tied weights, both sources of gradient contribute to the same parameters. This provides more learning signal per occurrence. This is particularly useful for vocabulary items that are infrequent as either input or output but not both.

Gradient Flow

With tied weights, the embedding matrix receives gradients from two sources:

  1. Input gradients: Backpropagated through the transformer from the loss. When a token appears in the input, its embedding affects all downstream computations, and gradients flow backward through all the transformer layers to update the embedding.

  2. Output gradients: Direct gradients from the output projection. When a token should be predicted, the dot product between the hidden state and that token's embedding directly contributes to the cross-entropy loss. This provides a gradient that says "move this embedding closer to the current hidden state."

This double gradient flow can be viewed as implicit multi-task learning. The embedding must simultaneously satisfy two objectives: representing tokens well for input processing so the transformer can reason about them, and providing good targets for output prediction so the model can accurately identify which token to generate next.

In[22]:
Code
class GradientTracker(nn.Module):
    """Track gradient magnitudes from different sources."""

    def __init__(self, vocab_size, hidden_dim):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, hidden_dim)
        self.fc = nn.Linear(hidden_dim, hidden_dim)  # Simulates transformer

    def forward(self, token_ids, return_hidden=False):
        # Input path
        x = self.embedding(token_ids)
        hidden = self.fc(x)

        # Output path (tied)
        logits = torch.matmul(hidden, self.embedding.weight.T)

        if return_hidden:
            return logits, hidden
        return logits


# Track gradients
grad_model = GradientTracker(1000, 64)
tokens = torch.randint(0, 1000, (4, 16))
labels = torch.randint(0, 1000, (4, 16))

logits = grad_model(tokens)
loss = F.cross_entropy(logits.view(-1, 1000), labels.view(-1))
loss.backward()

grad_magnitude = grad_model.embedding.weight.grad.abs().mean()
Out[23]:
Console
Mean gradient magnitude on tied embedding: 0.001368

This gradient combines contributions from both:
  - Forward pass through the input embedding
  - Backward pass through the output projection

Gradient norm: 0.9875
With separate embeddings, each matrix would receive only half this signal.

The gradient magnitude shows how much the embedding weights would change in a single training step (before learning rate scaling). With tied weights, this gradient is typically larger than it would be with separate embeddings because it aggregates signals from both the input and output paths. This can speed up learning for rare tokens that might otherwise receive sparse gradient updates, since each appearance provides double the learning signal.

The combined gradient also has a regularizing effect. The input gradient pushes the embedding toward representations that make downstream processing easier. The output gradient pushes it toward representations that align well with the hidden states the transformer produces for contexts where this token is appropriate. When both gradients point in compatible directions, the embedding is in a region of representation space that satisfies both constraints simultaneously, which is exactly where you want it.

Embedding Scale

A subtle issue arises with weight tying: the optimal scale for input embeddings may differ from the optimal scale for output projections. This affects real training dynamics and model performance.

Input embeddings are often scaled by d\sqrt{d} before being added to positional encodings, following the original transformer paper by Vaswani et al. (2017). The scaling formula is:

hscaled=dE[t]\mathbf{h}_{\text{scaled}} = \sqrt{d} \cdot \mathbf{E}[t]

where:

  • hscaled\mathbf{h}_{\text{scaled}}: the scaled input embedding that enters the transformer
  • dd: the embedding dimension
  • E[t]\mathbf{E}[t]: the raw embedding lookup for token tt
  • d\sqrt{d}: the scaling factor, which counterbalances the variance reduction that occurs when embeddings are initialized with small standard-deviation values

Without this scaling, embeddings initialized with small weights (standard deviation around 0.02) would be numerically negligible compared to the positional encodings, which are also on a small scale. The positional encodings would dominate the input signal, preventing the model from distinguishing token identities. The d\sqrt{d} scaling amplifies the token embeddings to a magnitude that balances both sources of information.

However, if we applied this scaling directly to the weight matrix, it would also affect the output logits when using tied weights. Scaled embeddings as output targets would produce much larger logits than expected, potentially destabilizing softmax and making the output distribution unreasonably peaked.

Modern implementations handle this by applying scaling at input time rather than modifying the embedding matrix itself:

In[24]:
Code
class ScaledTiedEmbeddings(nn.Module):
    """
    Tied embeddings with proper scaling.

    Input embeddings are scaled up, but output projection uses raw weights.
    """

    def __init__(self, vocab_size, hidden_dim):
        super().__init__()
        self.hidden_dim = hidden_dim
        self.embedding = nn.Embedding(vocab_size, hidden_dim)
        self.scale = np.sqrt(hidden_dim)

    def embed(self, token_ids):
        """Scaled embedding for input."""
        return self.embedding(token_ids) * self.scale

    def project(self, hidden_states):
        """Unscaled projection for output."""
        return torch.matmul(hidden_states, self.embedding.weight.T)


scaled_model = ScaledTiedEmbeddings(vocab_size, hidden_dim)
test_input = torch.randint(0, vocab_size, (1, 10))

# Forward pass
input_embeds = scaled_model.embed(test_input)
# ... transformer processing would happen here ...
output_logits = scaled_model.project(
    input_embeds
)  # Using embeds as proxy for hidden state
Out[25]:
Console
Input embedding scale factor: 27.71
Input embedding norm (per token): 765.35
Raw (unscaled) embedding norm:    27.62

The scale is applied at input time, not in the embedding matrix itself,
so the output projection sees the unscaled embeddings.

Scale ratio: 27.71x amplification for input path

The scale factor of approximately 27.7 for hidden dimension 768 significantly amplifies the input embeddings. By applying this scaling at runtime rather than baking it into the weights, we decouple the input and output requirements. The transformer processes scaled embeddings while the output projection uses raw embeddings, allowing each path to operate at its optimal magnitude. This is an example of the broader principle that tying weights does not mean treating both uses identically; you can still apply path-specific transformations.

When to Tie Weights

Weight tying is not always the right choice. Knowing when to apply it requires understanding both the benefits and the situations where the constraints it imposes cause more harm than good.

The benefits are clearest when input and output vocabularies are identical and the model needs to be efficient. Language modeling is the canonical case: you read text and generate text in the same language using the same tokens. The input and output domains are perfectly symmetric, and there is no principled reason why token representations should differ between these roles.

The constraints hurt most when input and output require different representations. Machine translation is the clearest counterexample: if you are translating from English to French using separate tokenizers, you cannot tie embeddings because the vocabularies are literally different. But even with a shared multilingual vocabulary, tying may be suboptimal for translation because the optimal encoding for reading an English source sentence may differ from the optimal target for generating a French output sentence. The distributional statistics of English and French are different enough that forcing a single shared representation for all tokens may limit what the model can learn.

Fine-tuning scenarios present another consideration. If you are fine-tuning a model that was pre-trained without weight tying, introducing tying during fine-tuning would require merging two separately optimized matrices, which typically hurts performance. The safest approach is to match the architecture of the pre-trained model.

In[26]:
Code
def recommend_weight_tying(
    vocab_size, hidden_dim, num_layers, same_io_vocab=True
):
    """Simple heuristic for weight tying recommendation."""

    embedding_params = vocab_size * hidden_dim
    # Rough estimate of transformer layer params
    layer_params = 12 * hidden_dim * hidden_dim  # Approximate
    total_layer_params = num_layers * layer_params

    embedding_ratio = embedding_params / (
        total_layer_params + 2 * embedding_params
    )

    recommendation = {
        "tie_weights": same_io_vocab and embedding_ratio > 0.05,
        "embedding_ratio": embedding_ratio,
        "reasoning": [],
    }

    if not same_io_vocab:
        recommendation["reasoning"].append(
            "Different input/output vocabularies - cannot tie"
        )
    elif embedding_ratio > 0.15:
        recommendation["reasoning"].append(
            "Embeddings are >15% of model - tying highly recommended"
        )
    elif embedding_ratio > 0.05:
        recommendation["reasoning"].append(
            "Embeddings are 5-15% of model - tying recommended"
        )
    else:
        recommendation["reasoning"].append(
            "Embeddings are <5% of model - tying optional"
        )

    return recommendation
Out[27]:
Console
Weight Tying Recommendations:
=================================================================

GPT-2 Small:
  Embedding ratio: 23.8%
  Recommendation: Tie
  Reason: Embeddings are >15% of model - tying highly recommended

GPT-3 175B:
  Embedding ratio: 0.4%
  Recommendation: Do not tie
  Reason: Embeddings are <5% of model - tying optional

Multilingual MT:
  Embedding ratio: 23.2%
  Recommendation: Do not tie
  Reason: Different input/output vocabularies - cannot tie

The heuristic reveals an important pattern: smaller models like GPT-2 Small have embeddings that constitute a significant fraction of total parameters (over 15 percent), making weight tying highly impactful. For massive models like GPT-3 175B, embeddings are less than 1 percent of parameters, so tying provides minimal savings. However, even large models typically use weight tying because it rarely hurts performance and provides a small memory benefit along with improved gradient flow. The multilingual translation case shows when tying is impossible: different input and output vocabularies require separate embedding spaces.

Practical Guidelines

When making the tying decision in practice, consider these factors:

  • Same vocabulary: Tying is only meaningful when input and output tokens come from the same vocabulary. This is true for language modeling, text classification with output over vocabulary, and sequence generation in the same language.

  • Model size: For models where embeddings exceed 10 percent of total parameters, tying provides noticeable savings. For very large models, the savings are proportionally smaller but the qualitative benefits of consistent representations remain.

  • Training from scratch: When training from scratch, tying is generally the right default choice. The model will naturally learn representations that satisfy both constraints simultaneously.

  • Fine-tuning: Match the pre-trained model's configuration. Changing the tying configuration during fine-tuning disrupts the learned representation structure.

  • Domain symmetry: If the input and output domains are symmetric (reading and writing in the same language, generating the same types of content you process as input), tying is appropriate. If there is an asymmetry in what optimal input versus output representations look like, consider whether untied weights might give the model more flexibility.

Historical Context

Historical Context

Weight tying was not part of the original language model designs but emerged as a practical insight from the word embedding era. Early neural language models from Bengio et al. (2003) used separate input and output weight matrices without questioning this design choice. The idea of sharing weights gained traction as researchers noticed that the two matrices often developed similar structures after training.

Press and Wolf (2017) formalized the connection in their paper "Using the Output Embedding to Improve Language Models," which demonstrated convincingly that tying the input embedding and output projection improved perplexity across multiple language modeling benchmarks. Simultaneously, Inan et al. (2017) independently published similar findings with additional theoretical analysis in "Tying Word Vectors and Word Classifiers: A Loss Framework for Language Modeling."

Both papers appeared before the transformer architecture became dominant, establishing weight tying as a general principle for neural language models rather than a transformer-specific optimization. When the transformer was introduced in 2017 and scaled up into models like GPT-2 in 2019, weight tying was adopted as a default, cementing its place as a standard component of modern language model design. The technique is now so widespread that most practitioners implement it without knowing its research origin.

Limitations and Impact

Weight tying represents one of those elegant techniques where reducing complexity improves results. The constraint that input and output embeddings share the same learned representation forces the model to develop more coherent internal semantics, and the research record shows consistent improvements in perplexity when tying is applied appropriately.

The primary limitation of weight tying is inflexibility. When input and output tasks require different token representations, tied weights create tension that the model cannot resolve without compromising both objectives. Machine translation between languages with different scripts is the canonical example: the optimal encoding for reading Japanese may differ substantially from the optimal target representation for generating English output, even if both pass through the same shared vocabulary of BPE tokens. In such cases, untied weights give the model the freedom to specialize each matrix for its respective task. Forcing a single matrix to serve both roles when the tasks differ is asking the model to make an impossible compromise.

There is also a subtler capacity argument. Very large models may benefit from the additional expressiveness of separate embeddings. When parameter count is less constrained, the regularization effect of tying matters less, and the model might learn better with independent representations that can specialize. However, the empirical evidence here is mixed. GPT-3 at 175 billion parameters uses weight tying, suggesting that even at that scale, the benefits of consistency outweigh the benefits of separate specialized matrices. The general consensus in the field is that tying is appropriate unless you have a specific reason to untie, such as different vocabularies or clear empirical evidence that untied weights perform better for your specific task.

A practical limitation arises in fine-tuning scenarios. If a model was pre-trained without weight tying and you want to apply weight tying during fine-tuning to reduce memory usage, you face the problem of how to initialize the shared matrix from two separately optimized matrices. Simply averaging them or choosing one over the other disrupts the pre-trained representations, often hurting performance. This makes weight tying a design choice that should be made at the beginning of training rather than retrofitted later.

The embedding scale issue discussed earlier in this chapter is also a practical limitation. The d\sqrt{d} scaling required for numerical stability during input processing is not appropriate for the output projection path. Implementations must carefully apply scaling at the right point in the forward pass, and this requirement is easy to get wrong when implementing weight tying for the first time. Getting the scaling wrong can cause training instability or suboptimal performance even when the conceptual implementation is correct.

Despite these limitations, weight tying's impact on the field has been substantial. It has become so universally adopted that its presence is often assumed rather than stated in architecture descriptions. The technique has influenced thinking about what neural networks are learning: if input and output embeddings converge toward similar values during training, that is evidence that the model is discovering a unified representation space rather than two separate approximations to the same underlying concept. Weight tying simply makes this convergence explicit and mandatory, saving parameters and accelerating learning in the process.

The broader lesson from weight tying is about recognizing and exploiting symmetry in model architecture. Whenever two components of a model are doing conceptually similar things, tying their weights may be beneficial. This principle extends beyond embeddings to other shared-weight architectures, like Siamese networks for similarity tasks or shared encoder components in multi-task models. Weight tying is a specific trick that illustrates a general principle: structural constraints reflecting meaningful symmetries tend to produce better models.

Key Parameters

When implementing weight tying in your models, these are the key configuration choices:

  • tie_weights (bool): Whether to share the embedding matrix between input and output. Set to True for most language models; set to False when input and output vocabularies differ or when fine-tuning models trained without tying.

  • vocab_size (int): Size of the vocabulary. Larger vocabularies increase the parameter savings from tying. With 50K tokens, embedding matrices can dominate smaller models. With 128K tokens, the embedding fraction rises significantly even for medium-depth models.

  • hidden_dim or d_model (int): The embedding and hidden dimension. This determines both the embedding matrix size (V×dV \times d) and the scale factor (d\sqrt{d}) used for input scaling.

  • embedding_scale (float): Scaling factor applied to input embeddings, typically d\sqrt{d}. Apply this at forward time rather than modifying the weight matrix, in order to preserve unscaled embeddings for the output projection path.

  • bias (bool): Whether to include a bias term in the output projection. Most implementations set this to False when using tied weights. A bias vector would add VV parameters and would not be shared, partially defeating the purpose of tying.

  • tie_encoder_decoder (bool, encoder-decoder models only): Whether to also tie the encoder input embedding with the decoder embeddings. Recommended when source and target vocabularies are identical, as in T5 and other monolingual sequence-to-sequence models.

Summary

Weight tying exploits the structural symmetry between input embedding and output projection matrices in language models. Both matrices have shape V×dV \times d, where VV is vocabulary size and dd is the hidden dimension. Instead of maintaining separate matrices E\mathbf{E} for input and Wout\mathbf{W}_{\text{out}} for output, weight tying sets Wout=E\mathbf{W}_{\text{out}} = \mathbf{E}, so a single shared matrix serves both roles.

The key insights from this chapter:

  • Parameter reduction: Weight tying eliminates one full embedding matrix, saving V×dV \times d parameters. For a 50K vocabulary with 768-dimensional embeddings, that is 38 million fewer parameters, often more than 15 percent of a small model's total capacity.

  • Theoretical justification: Input and output embeddings both answer the question "what does this token mean?" Tying them forces a consistent semantic representation where the hidden state that produces a token is similar to that token's embedding. This is not a constraint; it is a clarification of what good representations should look like.

  • Dot product as similarity: With tied weights, generating a token corresponds to finding which embedding most closely aligns with the current hidden state. The transformer's task is to produce hidden states that are similar to the embeddings of the correct next tokens.

  • Gradient dynamics: Tied weights receive gradients from both the input path (through transformer layers) and the output path (directly from cross-entropy loss). This provides more learning signal per token occurrence and acting as implicit multi-task learning.

  • Encoder-decoder extension: In sequence-to-sequence models, all three embedding matrices (encoder input, decoder input, decoder output) can share weights when the vocabulary is shared, eliminating two-thirds of embedding parameters. T5 uses this approach.

  • Embedding scale: The d\sqrt{d} scaling required for input stability should be applied at forward time, not baked into the weight matrix, so that the output projection sees unscaled embeddings.

  • When to use: Weight tying is recommended when vocabularies are large relative to model depth and input and output domains are symmetric. Avoid it when vocabularies differ, when fine-tuning models trained without tying, or when empirical evidence in your specific task suggests separate matrices perform better.

Modern language models nearly universally adopt weight tying, making it a fundamental architectural decision rather than an optimization. Understanding why it works deepens your understanding of what these models are learning: a unified representation space where token identity is consistently encoded whether a token is being read or generated.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about weight tying in language models.

Weight Tying Quiz

Question 1 of 100 of 10 completed
What is the primary benefit of weight tying in language models?

Comments

No comments yet. Be the first to share your thoughts!

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2025weighttying, author = {Michael Brenndoerfer}, title = {Weight Tying in Transformer Embeddings}, year = {2025}, url = {https://mbrenndoerfer.com/writing/weight-tying-shared-embeddings-transformers}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-09-15} }
APAAcademic
Michael Brenndoerfer (2025). Weight Tying in Transformer Embeddings. Retrieved from https://mbrenndoerfer.com/writing/weight-tying-shared-embeddings-transformers
MLAAcademic
Michael Brenndoerfer. "Weight Tying in Transformer Embeddings." 2026. Web. September 15, 2026. <https://mbrenndoerfer.com/writing/weight-tying-shared-embeddings-transformers>.
CHICAGOAcademic
Michael Brenndoerfer. "Weight Tying in Transformer Embeddings." Accessed September 15, 2026. https://mbrenndoerfer.com/writing/weight-tying-shared-embeddings-transformers.
HARVARDAcademic
Michael Brenndoerfer (2025) 'Weight Tying in Transformer Embeddings'. Available at: https://mbrenndoerfer.com/writing/weight-tying-shared-embeddings-transformers (Accessed: September 15, 2026).
SimpleBasic
Michael Brenndoerfer (2025). Weight Tying in Transformer Embeddings. https://mbrenndoerfer.com/writing/weight-tying-shared-embeddings-transformers

About the author

Continue with the full handbook

This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.

Explore Language AI Handbook
Newsletter

Stay up to date

Get articles, book updates, and news delivered to your inbox.

No spam, unsubscribe anytime.

or

Join the community

Sign in to remove popups, track your reading progress, and join the discussion.