N-grams were introduced by Shannon, and fastText turned them into learned vectors. Hash Embeddings hashed them into tables and Product Key Memory introduced large, sparsely accessed memory within transformers. Engram from DeepSeek is the culmination of all this work.
The fundamental idea is to give the model a space where it can store information about relationships between local n-grams to avoid spending FLOPs computing these relationships. The idea is that this will allow the model to put more capacity elsewhere. Trading off this memory for FLOPs works as the memory look ups are cheap.
We start by sliding a window (stride 1) over our token sequence and extracting every run of
The hashing can be implemented as a simple multiplicative XOR hash. We keep separate tables for the bigrams and trigrams and for each of these, we split the hashing over multiple tables to avoid hash collisions. For example, we could have 4 tables per n-gram order, giving us a total of 8 tables. We would produce 8 hashes in total and look up 8 embedding vectors. These are later projected to the model dimension
We'd like to give the model some control over how much of the Engram embedding vector it wants, so we compute a gate using a projection and a dot product using the following method:
Where
Once we have
Note that
With mHC, we have
To ensure that n-grams for the same word written in different formats (toddler vs TODDLER) end up in the same lookup, we need to ensure the tokeniser produces the same tokens for them. Since subword tokenisers prioritise lossless compression, they end up assigning different token IDs to the same word, even if one is a capitalised version of the other, which learns separate embeddings for the same word, increasing table and model size. To resolve this concern, DeepSeek applies the following text normalisation rules:
In practice, we need a mapping table which maps original token IDs to compressed IDs. We build this mapping table once at the start of training by going over our vocab, normalising text, storing their original IDs and creating compressed IDs for them. Once the mapping is created, during inference and training, we simply do an array lookup, using the original IDs to retrieve the compressed IDs, fast and cheap.
Below is a naive PyTorch implementation, not including the hashing and compression functions as those are pretty standard. A few notes to help understand the implementation:
self.get_embeddings(n_grams) does the lookup using the hashes to get K × n_orders embedding vectors, which are concatenated together to form a vector of size K is the number of tables per n-gram order and n_orders is the number of n-gram orders. If we did bigram and trigram with four tables each, we'd look up 8 vectors of size
class Engram(nn.Module):
def __init__(self, cfg):
super().__init__()
self.d_model = cfg["d_model"]
self.n = cfg["n"]
# Memory projections: [b, t, d_mem] -> [b, t, d_model]
self.W_V = nn.Linear(cfg["d_mem"], cfg["d_model"], bias=True)
self.W_K = nn.ModuleList([
nn.Linear(cfg["d_mem"], cfg["d_model"], bias=True)
for _ in range(self.n)
])
self.norm_h = nn.RMSNorm(cfg["d_model"])
self.norm_k = nn.RMSNorm(cfg["d_model"])
def engram_context_aware_gating(self, n_grams, x_res):
# Look up hashed N-gram embeddings and concatenate them
# [b, t, n_orders, K] -> [b, t, d_mem]
e = self.get_embeddings(n_grams)
# Project memory into residual-stream dimension
# [b, t, d_mem] -> [b, t, d_model]
v = self.W_V(e)
# Apply independent gates to each mHC residual stream
# n * [b, t, d_model] -> [b, t, n, d_model]
return torch.stack([
self.context_gate(x_res[:, :, m, :], e, v, m)
for m in range(self.n)
], dim=2)
def context_gate(self, h, e, v, branch):
# Project memory to a branch-specific key
# [b, t, d_mem] -> [b, t, d_model]
k = self.W_K[branch](e)
# Compute normalized query-key similarity
# [b, t, d_model] -> [b, t, 1]
z = (
self.norm_h(h) * self.norm_k(k)
).sum(-1, keepdim=True) / math.sqrt(self.d_model)
# Apply signed square-root scaling
# [b, t, 1] -> [b, t, 1]
z = z.abs().clamp_min(1e-6).sqrt() * z.sign()
# Convert similarity to a scalar gate per token
# [b, t, 1] -> [b, t, 1]
gate = torch.sigmoid(z)
# Gate memory values across all embedding dimensions
# [b, t, 1] * [b, t, d_model] -> [b, t, d_model]
v_gated = gate * v
# Return gated memory
# [b, t, d_model] -> [b, t, d_model]
return v_gated
The plot below shows the held-out cross-entropy loss of a vanilla 124M model vs the same model with mHC vs mHC plus Engram trained for 2.5B tokens on the FineWeb-Edu dataset. We can see a ~2.5% improvement in validation loss when combining Engram and mHC. For more details on how Engram behaves, please see the Nenya technical report.

Figure 1: A comparison of vanilla 124M transformer vs vanilla + mHC vs vanilla + mHC + Engram.