"The animal didn't cross the street because it was too tired."
What does "it" refer to — the animal, or the street?
You knew instantly. You attended to the right word.
For a decade, building a machine that could do the same
was one of the hardest problems in AI.
In 2017, eight engineers solved it with a single scaled dot product.
01 — The Central Idea: Every Word Talks to Every Other Word
Before transformers, language models processed text like reading a book through a keyhole — one word at a time, left to right. To connect "it" to "animal" in a 50-word sentence, information had to travel through 48 hidden state updates, each one potentially overwriting what came before. The further apart two words were, the harder it was to connect them.
The transformer throws this constraint away entirely. Its core idea is strikingly simple: let every word look at every other word at the same time, and let the model decide how much each pair is related. Not "word 1 then word 2 then word 3" — all words simultaneously, all relationships computed in parallel.
Imagine a sentence as a group of people in a room. In an old RNN model, they pass a message around a circle — person 1 whispers to person 2, who whispers to person 3. By the time the message reaches person 20, it is barely recognisable.
In a transformer, everyone in the room speaks at once. Every person can hear every other person. Each person decides how much attention to pay to each voice — based on what they personally need to figure out right now. "It" listens carefully for a noun it could refer to. "Animal" raises its voice in response. "Street" stays quiet. The word "it" updates its understanding based on what it heard — mostly "animal." This is attention.
| Problem | RNN / LSTM | Transformer |
|---|---|---|
| Long-range relationships | ✗ Degrade with distance — information passed through every intermediate word | ✓ Direct connection between any two words — distance is irrelevant |
| Training speed | ✗ Must process word-by-word — cannot parallelise across the sequence | ✓ All positions processed simultaneously — scales across thousands of GPUs |
| Gradient flow | ✗ Gradients travel through every timestep — vanish or explode | ✓ Direct gradient path between any two positions — stable at depth |
02 — The Core Question: How Does a Word Decide What to Pay Attention To?
Here is the problem the architects had to solve. You have a sentence: "The cat sat on the mat because it was comfortable." The word "it" needs to figure out what it refers to. It can look at every other word — but how does it score them? How does it decide "mat" is more relevant than "sat" here?
The answer the paper proposed is elegant: split each word's representation into three different roles, and use those roles to compute relationships. Every word simultaneously acts as:
- Something that is looking for relevant information from others
- Something that announces what it can offer to those looking
- Something that delivers the actual information once it is selected
These three roles are named Query, Key, and Value — borrowed from the world of databases and information retrieval. Understanding what each one truly means is the key to understanding the entire transformer.
03 — Q, K, V: What They Are and Why They're Named That Way
You search for books about "renewable energy." You type your search term into the system — that is your Query. Every book has a spine label: title, keywords, subject — that is its Key. The system compares your Query against every Key and scores how well each book matches.
Now — the crucial part. The book you pull from the shelf does not contain the keywords on its spine. It contains the actual text — that is the Value. The Key was just the index label used for matching. The Value is what you actually read.
In attention: Query = your search. Key = each word's index label. Value = what each word actually contributes once selected. You match Query against Keys to decide relevance. Then you collect the Values of the most relevant words.
Why Are Key and Value Two Separate Things?
This is the question most explanations skip — and it is the most important one. Why not just match directly on the word embedding, without the Key/Value separation?
The answer is flexibility and context-sensitivity. A word needs to advertise itself differently depending on the relationship type being computed, and deliver different content depending on who is asking. The Key and Value are both learned projections of the same word — but through different weight matrices, which means the model learns to separate "how I want to be found" from "what I give when found."
1. "She sat on the bank of the river."
2. "He deposited money at the bank."
In sentence 1, when "river" queries for a related geographic feature, "bank" should respond with a Key that says "I am a natural location feature." In sentence 2, when "deposited" queries for a financial entity, "bank" should respond with a Key that says "I am a financial institution."
The Values are equally different — what "bank" contributes to nearby words is completely different in each sentence. The Key controls whether "bank" gets selected; the Value controls what "bank" contributes once selected. Separating them lets the model learn both independently through W_K and W_V — two matrices with different roles, trained on the same word representation.
This is why the same word in different contexts produces dramatically different final representations. The Key advertises contextually; the Value delivers contextually. Both are powered by the same input embedding, but each is shaped by its own learned transformation.
The Three Projections — The Learned Matrices
Every word in the sequence produces all three vectors through three separate learned linear transformations (W_Q, W_K, W_V) — weight matrices trained during pre-training:
K = X · W_K ← "What can I offer / how to find me?" W_K ∈ ℝ^(d_model × d_k)
V = X · W_V ← "What is my actual content?" W_V ∈ ℝ^(d_model × d_v)
X = token embeddings, shape (seq_len × d_model)
Every word simultaneously produces Q, K, and V for itself.
This is "self-attention" — every word attends to every other word, including itself.
W_Q, W_K, W_V are learned parameters — the model figures out what questions to ask,
how to advertise, and what to deliver, entirely from training data.
04 — The Full Attention Computation: Step by Step
Now that Q, K, and V make intuitive sense, let us trace the full computation. Four steps, each with a clear purpose.
Step 1 — Raw Scores: How Well Does Each Query Match Each Key?
Dot product every token's Query against every other token's Key. The higher the dot product, the more aligned the two vectors are — the more relevant that word is to the query.
Result: an (n × n) matrix — entry [i, j] = how much word i wants to attend to word j
n = sequence length. All pairs computed simultaneously.
Step 2 — Scale: Prevent Softmax Saturation
Two random d_k-dimensional vectors have a dot product with std dev √d_k
For d_k=64: σ≈8. For d_k=512: σ≈22. Unscaled scores are too large.
Large scores → softmax assigns ≈1 to the max, ≈0 to everything else
→ gradient of softmax ≈ 0 everywhere → training stalls
Dividing by √d_k keeps scores at ≈unit variance regardless of dimension size.
Step 3 — Softmax: Convert Scores to Attention Weights
Each row sums to 1.0 — a probability distribution over the sequence
"it" might produce weights: [0.03, 0.07, 0.03, 0.02, 0.55, 0.01, 1.00, 0.02, 0.27]
The cat sat on mat bec it was comf
→ "mat" dominates. "it" will absorb mostly mat's content.
Step 4 — Weighted Sum of Values: Collect the Content
Each word's new representation = weighted blend of ALL words' Value vectors
"it" absorbs 55% of "mat"'s Value, 27% of "comfortable"'s Value, etc.
The word "it" now carries context from the words it attended to — in its new vector.
All four steps in one formula — the heart of every transformer ever built:
GPT-4, Claude, Gemini, LLaMA-3 — all of them are, at their core, this formula repeated billions of times.
05 — Worked Numeric Example: Seeing It Happen
Full computation for a 3-word sequence "Cat sat mat", d_k = 2. The Q, K, V values below are what the model produces after projecting through W_Q, W_K, W_V.
Step 1: Raw scores for "Cat" (row 0 of Q · Kᵀ)
Cat→Cat: (1.0×1.0)+(0.5×0.2) = 1.10 ← Cat querying itself
Cat→sat: (1.0×0.5)+(0.5×1.0) = 1.00 ← Cat querying "sat"
Cat→mat: (1.0×0.2)+(0.5×0.8) = 0.60 ← Cat querying "mat"
Step 2: Scale by 1/√2 ≈ 0.707
[1.10, 1.00, 0.60] × 0.707 = [0.778, 0.707, 0.424]
Step 3: Softmax → attention weights
exp([0.778, 0.707, 0.424]) = [2.177, 2.028, 1.528] sum = 5.733
weights = [0.380, 0.354, 0.266]
↑ "Cat" pays 38% to itself, 35.4% to "sat", 26.6% to "mat"
Step 4: Weighted sum of Values
0.380 × [2.0, 0.0] = [0.760, 0.000] ← from Cat's Value
0.354 × [0.0, 2.0] = [0.000, 0.708] ← from sat's Value
0.266 × [1.0, 1.0] = [0.266, 0.266] ← from mat's Value
─────────────────────────────────────
"Cat" new representation = [1.026, 0.974]
"Cat" started as its own isolated embedding. After one attention operation, its representation is a contextual blend: 38% of its own content, 35.4% of "sat"'s content (it did something), 26.6% of "mat"'s content (there was a location). The word "Cat" now encodes relationships — its representation is context-aware.
Scale to 768 dimensions, 12 heads, 12 layers, 30,000 vocabulary items: BERT. Scale to 12,288 dimensions, 96 heads, 96 layers: GPT-3. The operation is identical. Only the size changes.
06 — Multi-Head Attention: Attending to Many Relationships at Once
Single-head attention produces one score per token pair — one relationship type. But language has many simultaneous relationship types: syntactic (subject-verb agreement), semantic (synonymy), coreference (pronoun resolution), positional (nearby context). A single head must average over all of them. It is like trying to answer multiple questions from a group at once — you miss the nuance.
Multi-head attention runs H independent attention operations in parallel, each with its own W_Q, W_K, W_V. Each head learns to specialise in a different type of relationship.
Imagine giving a complex legal document to a panel: one logician (tracks if-then structure), one entity specialist (tracks who owes what to whom), one timeline expert (tracks before/after), one terminology specialist (tracks legal terms). All reading the same document simultaneously, each noting different things.
Each attention head is a specialist. Head 1 might learn to track subject-verb agreement. Head 2 learns coreference. Head 3 learns local context. Head 4 learns semantic similarity. They all process the same input — they just attend to different aspects of it.
head_h = Attention(X·W_Q^h, X·W_K^h, X·W_V^h)
d_k = d_model / H (each head works in a smaller subspace)
MultiHead(X) = Concat(head_1, …, head_H) · W_O
Concatenate all heads → shape (seq, d_model). W_O learns to mix information from all heads.
BERT-base: H=12, d_model=768, d_k=64 per head
GPT-3: H=96, d_model=12288, d_k=128 per head
07 — Positional Encodings: Adding the Sense of Order
Attention is position-blind. The dot product Q·Kᵀ measures semantic similarity — no notion of which word came first. "Cat sat mat" and "Mat sat cat" look identical to attention alone. Positional encodings add a position-dependent signal to each word's embedding before attention runs, breaking this symmetry.
| Method | How it works | Extrapolates beyond training? | Used by |
|---|---|---|---|
| Sinusoidal | Fixed sin/cos formula at different frequencies. No parameters. | ~ Degrades slowly | Original Transformer (2017) |
| Learned absolute | Lookup table trained alongside the model. Position 0 gets embedding P[0], etc. | ✗ Hard wall at max_seq_len | BERT (512), GPT-2 (1024) |
| RoPE | Rotates Q and K vectors by an angle proportional to position. Dot product encodes relative distance natively. | ✓ Extensible via YaRN to 128k+ | LLaMA, Mistral, Qwen, Gemma |
| ALiBi | Subtracts a distance penalty from attention scores. No parameters. | ✓ Graceful degradation | BLOOM (176B), MPT |
Day 25 covers all four in full depth — the math, the intuition, and why RoPE became the dominant choice for every modern LLM.
08 — Causal Masking: How GPT Stays Honest During Training
Standard attention lets every word see every other word — including future ones. For a model learning to predict the next word, seeing future words is cheating: you can just copy the answer. Decoder models (GPT, LLaMA, Claude) apply a causal mask: add −∞ to future positions before softmax. Since exp(−∞) = 0, future tokens get exactly zero weight.
Causal mask for seq_len=4:
tok1 tok2 tok3 tok4
tok1 [ 0.0, -inf, -inf, -inf] ← sees only itself
tok2 [ 0.0, 0.0, -inf, -inf] ← sees tok1 and itself
tok3 [ 0.0, 0.0, 0.0, -inf] ← sees tok1, tok2, itself
tok4 [ 0.0, 0.0, 0.0, 0.0] ← sees all previous
At inference: the model reads "The capital of France is" and predicts "Paris." It appends "Paris" and predicts the next token. One token at a time, left to right — exactly matching how it was trained.
09 — The Complete Transformer Block
A transformer stacks identical blocks. Each block has two sub-layers, each wrapped with a residual connection and layer normalisation:
x = x + MultiHeadAttention( LayerNorm(x) )
Sub-layer 2: Feed-Forward Network
x = x + FFN( LayerNorm(x) )
FFN(x) = GELU( x · W₁ + b₁ ) · W₂ + b₂
W₁: d_model → 4×d_model (expand). W₂: 4×d_model → d_model (contract).
10 — Code: Attention and Full Transformer Block from Scratch
import torch, math
import torch.nn as nn
import torch.nn.functional as F
# ─────────────────────────────────────────────────────────────────────────
# 1. Scaled Dot-Product Attention
# Q: what I'm looking for | K: what I can offer | V: what I deliver
# ─────────────────────────────────────────────────────────────────────────
def scaled_dot_product_attention(Q, K, V, mask=None):
d_k = Q.size(-1)
scores = (Q @ K.transpose(-2, -1)) / math.sqrt(d_k) # (B, H, T, T)
if mask is not None:
scores = scores + mask
weights = F.softmax(scores, dim=-1)
return weights @ V, weights
# ─────────────────────────────────────────────────────────────────────────
# 2. Multi-Head Attention — H heads, each specialising in different relationships
# ─────────────────────────────────────────────────────────────────────────
class MultiHeadAttention(nn.Module):
def __init__(self, d_model, n_heads):
super().__init__()
assert d_model % n_heads == 0
self.d_k = d_model // n_heads
self.n_heads = n_heads
self.W_q = nn.Linear(d_model, d_model, bias=False) # what am I looking for?
self.W_k = nn.Linear(d_model, d_model, bias=False) # what can I offer?
self.W_v = nn.Linear(d_model, d_model, bias=False) # what do I deliver?
self.W_o = nn.Linear(d_model, d_model, bias=False) # mix all heads' outputs
def forward(self, x, mask=None):
B, T, C = x.shape
def split(t):
return t.view(B, T, self.n_heads, self.d_k).transpose(1, 2)
Q, K, V = split(self.W_q(x)), split(self.W_k(x)), split(self.W_v(x))
out, attn = scaled_dot_product_attention(Q, K, V, mask)
out = out.transpose(1, 2).contiguous().view(B, T, C)
return self.W_o(out), attn
# ─────────────────────────────────────────────────────────────────────────
# 3. Feed-Forward Network (position-wise, 4× expansion, GELU)
# ─────────────────────────────────────────────────────────────────────────
class FeedForward(nn.Module):
def __init__(self, d_model):
super().__init__()
self.net = nn.Sequential(
nn.Linear(d_model, d_model * 4),
nn.GELU(),
nn.Linear(d_model * 4, d_model),
)
def forward(self, x): return self.net(x)
# ─────────────────────────────────────────────────────────────────────────
# 4. Transformer Block (Pre-LN variant — more stable than original Post-LN)
# ─────────────────────────────────────────────────────────────────────────
class TransformerBlock(nn.Module):
def __init__(self, d_model, n_heads, dropout=0.1):
super().__init__()
self.ln1 = nn.LayerNorm(d_model)
self.attn = MultiHeadAttention(d_model, n_heads)
self.ln2 = nn.LayerNorm(d_model)
self.ffn = FeedForward(d_model)
self.drop = nn.Dropout(dropout)
def forward(self, x, mask=None):
attn_out, w = self.attn(self.ln1(x), mask)
x = x + self.drop(attn_out) # residual
x = x + self.drop(self.ffn(self.ln2(x)))
return x, w
# ─────────────────────────────────────────────────────────────────────────
# 5. Sinusoidal Positional Encoding
# ─────────────────────────────────────────────────────────────────────────
class SinusoidalPE(nn.Module):
def __init__(self, d_model, max_len=4096):
super().__init__()
pe = torch.zeros(max_len, d_model)
pos = torch.arange(0, max_len).unsqueeze(1).float()
div = torch.exp(torch.arange(0, d_model, 2).float() * -(math.log(10000.0) / d_model))
pe[:, 0::2] = torch.sin(pos * div)
pe[:, 1::2] = torch.cos(pos * div)
self.register_buffer('pe', pe.unsqueeze(0))
def forward(self, x): return x + self.pe[:, :x.size(1)]
# ─────────────────────────────────────────────────────────────────────────
# Sanity check
# ─────────────────────────────────────────────────────────────────────────
d_model, n_heads, seq_len = 128, 4, 16
pe = SinusoidalPE(d_model)
block = TransformerBlock(d_model, n_heads)
x = pe(torch.randn(2, seq_len, d_model))
mask = torch.triu(torch.full((seq_len, seq_len), float('-inf')), diagonal=1)
out, attn_w = block(x, mask=mask)
print(f"Input shape: {x.shape}")
print(f"Output shape: {out.shape}")
print(f"Attn weight shape: {attn_w.shape}")
print(f"Params in block: {sum(p.numel() for p in block.parameters()):,}")
print(f"Causal mask OK: {(attn_w[0, 0, 0, 1:] == 0).all().item()}")
print(f"Attn weights sum: {attn_w[0, 0, 0].sum().item():.4f}")
Input shape: torch.Size([2, 16, 128])
Output shape: torch.Size([2, 16, 128]) ← shape preserved (d_model in = d_model out)
Attn weight shape: torch.Size([2, 4, 16, 16]) ← (batch, heads, seq, seq)
Params in block: 264,448
Causal mask OK: True ← position 0 cannot attend to future positions
Attn weights sum: 1.0000 ← each row sums to 1 (valid probability distribution)
11 — KV-Cache: Why Chat Generation Is Fast
During text generation, the model produces one token at a time. Without optimisation, it would recompute K and V for every token in context at every step. The KV-cache stores K and V once computed and reuses them. Only the new token's K and V need to be computed each step.
This reduces generation complexity from O(n²) (recompute everything) to O(n) (compute one row, reuse all others). For a 128k-context conversation, the difference is seconds vs minutes per response.
Why cache K and V, but not Q? Because K and V represent fixed properties of each context token — "what I can offer" and "what I deliver" don't change once a token is in the sequence. Q represents what the current generation step is looking for — it changes at every step as the model produces the next token.
12 — Encoder vs Decoder vs Encoder-Decoder
| Architecture | Attention type | Training task | Best for | Examples |
|---|---|---|---|---|
| Encoder-only | Bidirectional — every word sees every word | Masked Language Modelling (MLM) | Understanding: classification, NER, search, embeddings | BERT, RoBERTa, DeBERTa |
| Decoder-only | Causal — each word sees only past words | Next-token prediction (CLM) | Generation: text, code, reasoning, chat | GPT-4, Claude, LLaMA, Mistral |
| Encoder-decoder | Encoder: bidirectional. Decoder: causal + cross-attention to encoder | Sequence-to-sequence | Translation, summarisation, speech-to-text | T5, BART, Whisper |
13 — A Thought to Sit With
The transformer was motivated by one linguistic problem: resolving references across long distances. It worked — and then it turned out to work equally well on images, protein structures, audio spectrograms, and video. Domains with completely different structures, governed by completely different physics, all amenable to the same scaled dot product.
What does this universality reveal? Is attention capturing something fundamental about the structure of information — that meaningful patterns in any domain can be expressed as weighted relationships between parts? Or did eight engineers get extraordinarily lucky with a formulation general enough to fit whatever gradient descent was minimising — and we've been reverse-engineering a justification ever since?
14 — Coming Up Next
Tomorrow — Day 25: Positional Encodings. Attention is permutation-blind by design — shuffle the tokens and you get the same result. Positional encodings fix this: sinusoidal patterns, learned absolute positions, and the rotary embeddings (RoPE) used in LLaMA, Mistral, and Gemma that encode position as a vector rotation — naturally extensible to sequences far longer than training.
15 — Go Deeper
▶ But what is a GPT? Visual intro to Transformers — 3Blue1Brown
YouTube · 27 min · The best visual explanation of attention available. The Q/K/V segment is particularly clear. Watch alongside this post.
🔗 The Illustrated Transformer — Jay Alammar
Step-by-step visual walkthrough. The multi-head attention and encoder-decoder diagrams are the clearest available.
📄 Attention Is All You Need — Vaswani et al. (2017)
The original. Section 3.2 and Figure 2 are the core. The ablation table (Section 7) shows how each architectural choice was validated.
📄 FlashAttention — Dao et al. (2022)
The engineering that made long-context LLMs practical. Section 2 explains the IO-aware tiling. Essential for LLM inference work.
🔗 A Mathematical Framework for Transformer Circuits — Anthropic (2021)
Foundational mechanistic interpretability work — what do Q, K, V actually compute in a trained model? The heads-as-specialists findings come from here.
- The core idea: every word looks at every other word simultaneously. Relationships are computed directly between any pair — no sequential processing, no distance penalty.
- Q — Query: "What am I looking for?" Each token broadcasts a question to the sequence. Produced by multiplying the token embedding through learned matrix W_Q.
- K — Key: "What can I offer? How should I be found?" Each token's searchable label — controls whether it gets selected. Produced through W_K. Separate from Value because searchability should be independent of content.
- V — Value: "What is my actual content?" The payload delivered once a token is selected. Produced through W_V. The Key gets you found; the Value is what you give.
- The K/V separation insight: the same word in different contexts needs different Keys (advertise differently) and different Values (deliver different content). Learned independently through separate W_K and W_V matrices.
- Four computation steps: (1) Scores = Q·Kᵀ — relevance of every pair; (2) Scale by 1/√d_k — prevent softmax saturation and vanishing gradients; (3) Softmax → attention weights (prob. distribution per token); (4) Weighted sum of Values → new contextual representation.
- Multi-head attention: H heads, each with own W_Q/K/V, each specialising in a different relationship type. Same compute as single-head; H times richer representation. Outputs mixed through W_O.
- Causal masking: add −∞ to upper-triangle positions before softmax → future tokens get weight 0. Enables GPT-style autoregressive generation.
- Transformer block: Multi-Head Attention + FFN, each wrapped with residual connections (gradient highway) and LayerNorm (stable activations). Attention mixes across positions; FFN transforms within each position.
- KV-cache: K and V are fixed properties of context tokens — cache them once. Q changes every generation step — don't cache. Reduces generation cost from O(n²) to O(n).
- What is self-attention? What problem does it solve compared to RNNs?
Self-attention computes relationships between every token pair in a sequence simultaneously. Each token produces Q, K, V; Q is dotted against every K for relevance scores; softmax gives attention weights; output is a weighted sum of V vectors. Solves the long-range dependency problem: RNNs require information to travel through every intermediate hidden state — degrading over distance. In attention, word 2 and word 50 have a direct connection via Q·K dot product. Distance is irrelevant. - What are Q, K, and V? Why are Key and Value separate projections?
Q = what this token is looking for. K = how this token advertises itself to be found. V = what this token actually delivers once selected. They're separate because a token may need to control its searchability (Key) independently from its content (Value). "Bank" in "river bank" needs a geographic Key but delivers riparian content. In "money bank" it needs a financial Key but delivers financial content. Separate W_K and W_V matrices let the model learn these independently. If they were the same, a token couldn't adjust its searchability without changing its content. - Why does the formula divide by √d_k?
Two d_k-dimensional random vectors have a dot product with standard deviation √d_k. Without scaling, scores are too large, pushing softmax toward saturation (one score gets weight ≈1, all others ≈0). In this regime, the gradient of softmax is ≈0 — W_Q and W_K stop receiving meaningful gradient signal and training stalls. Dividing by √d_k keeps dot products at ≈unit variance regardless of dimension, maintaining softmax in its gradient-rich regime throughout training. - What is multi-head attention and what do different heads learn?
H independent attention operations in parallel, each with own W_Q/K/V, each in d_k=d_model/H subspace. Each head can specialise in a different relationship type. Research finds consistent specialisation: some heads track subject-verb agreement, some coreference, some bracket matching in code, some local context. Same total compute as single-head at full dimension, but H times richer — each head attends to orthogonal aspects. W_O after concatenation learns to optimally combine all heads. - What is the KV-cache and why is Q not cached?
The KV-cache stores K and V vectors for all context tokens, avoiding recomputation at each generation step. Without it, generating n tokens costs O(n²) total (recompute all K/V at each step); with it, O(n) (one new K/V per step, rest from cache). K and V are cached because they represent fixed properties of context tokens — what a token offers and delivers doesn't change once it's in the sequence. Q is not cached because it represents what the current generation step is looking for — this changes at every step as the model produces a new token.
▶ But what is a GPT? — 3Blue1Brown
YouTube · 27 min · Best visual explanation of attention. The Q/K/V segment is essential. Watch alongside this post.
🔗 The Illustrated Transformer — Jay Alammar
The most popular visual walkthrough. Every component drawn step by step.
📄 Attention Is All You Need — Vaswani et al. (2017)
The original paper. 15 pages. Section 3.2 is the core. The ablation table shows how each design choice was validated.
📄 FlashAttention — Dao et al. (2022)
The engineering that made 128k+ context practical. Essential for LLM inference work.
🔗 A Mathematical Framework for Transformer Circuits — Anthropic (2021)
What do Q, K, V actually compute in a trained model? The heads-as-specialists findings come from here.