Models From First Principles 00: The Model Inside the Model
The Model Inside the Model
This is the first post in Models From First Principles.
The previous PyTorch: Zero to Hero series worked from the bottom up.
We started with tensors.
Then gradients.
Then nn.Module.
Then data pipelines, convolution, attention, debugging, performance and finally a small GPT-style language model built from scratch.
That series answered:
What are the pieces?
This series asks a different question:
What happens when we start composing those pieces into increasingly sophisticated models?
The central idea is simple:
A complicated model is usually a collection of smaller models, and those smaller models are themselves collections of simpler operations.
If we keep decomposing, the mystery disappears.
A model becomes a few heads.
A head becomes a few linear layers.
A recurrent reasoning system becomes a state update repeated several times.
A hierarchical model becomes two state-update mechanisms running at different rates.
A custom optimizer becomes a pair of moving averages and a parameter update.
The point of this series is not to memorize architectures.
It is to learn how to read, decompose, build and reason about them.
1. Why another model series?
The term model has become almost uselessly broad.
When someone says “model”, they might mean:
- a frontier language model with hundreds of billions of parameters;
- a convolutional classifier;
- a tiny MLP that predicts a scalar;
- a value function;
- a policy head;
- a recurrent reasoning block;
- a sparse autoencoder;
- an ensemble;
- a calibration network;
- or a small learned component buried inside a much larger system.
That can make model architecture feel harder than it really is.
The word suggests one monolithic object.
The code usually tells a different story.
Consider this:
large model
↓
smaller sub-models
↓
blocks
↓
layers
↓
tensor operations
Once you can move up and down that hierarchy, a model stops being a black box.
That is the skill we are going to practise.
2. This series builds on PyTorch Zero to Hero
I am not going to re-explain every PyTorch primitive from scratch in every article.
When we see:
nn.Linear(256, 128)
we will assume you know what a linear layer is.
When we see:
loss.backward()
we will assume you understand autograd.
When we see:
class Model(nn.Module):
...
we will assume you understand parameter registration and state_dict().
When we see:
nn.MultiheadAttention(...)
we will not treat Q, K and V as magic.
The earlier series exists precisely so we can now work one level higher.
Instead of asking:
What does
nn.Lineardo?
we can ask:
Why is this linear layer here?
Instead of asking:
How does autograd work?
we can ask:
What objective is this model actually learning?
Instead of asking:
What shape does attention expect?
we can ask:
Why does this architecture need attention at all?
That is the shift.
3. The architecture lineage we will study
We are going to follow a sequence of increasingly sophisticated ideas.
The implementations that motivated this series grew out of real evaluation and reasoning models, but every article will stand independently.
You do not need the original system.
You do not need its database.
You do not need its agents.
You only need PyTorch and the willingness to inspect what the model is actually doing.
Our roadmap is roughly:
MR.Q
↓
EBT
↓
SICQL
↓
HRM
↓
Tiny
↓
PACS
This is not Python inheritance.
It is an evolution of modelling ideas.
Each step asks what the previous step could not express cleanly enough.
4. Stage one: turn two embeddings into one judgement
Suppose we have two pieces of text:
prompt
response
An embedding model turns each into a vector:
prompt → context embedding
response → response embedding
Now imagine that our job is to answer one question:
How good is this response for this prompt?
A very small model is enough to begin.
Conceptually:
context embedding
+
response embedding
↓
encoder
↓
predictor
↓
score
We can express that in PyTorch with almost nothing:
import torch
from torch import nn
class PairEncoder(nn.Module):
def __init__(self, embedding_dim: int, hidden_dim: int):
super().__init__()
self.net = nn.Sequential(
nn.Linear(embedding_dim * 2, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, hidden_dim),
)
def forward(self, context, response):
pair = torch.cat([context, response], dim=-1)
return self.net(pair)
class QualityPredictor(nn.Module):
def __init__(self, hidden_dim: int):
super().__init__()
self.net = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim // 2),
nn.ReLU(),
nn.Linear(hidden_dim // 2, 1),
)
def forward(self, z):
return self.net(z).squeeze(-1)
Then compose them:
class QualityModel(nn.Module):
def __init__(self, embedding_dim=1024, hidden_dim=256):
super().__init__()
self.encoder = PairEncoder(embedding_dim, hidden_dim)
self.predictor = QualityPredictor(hidden_dim)
def forward(self, context, response):
z = self.encoder(context, response)
return self.predictor(z)
That is already a useful model.
But notice what happened.
We said we had one model.
The code immediately decomposed into:
QualityModel
│
├── PairEncoder
│ ├── Linear
│ ├── ReLU
│ └── Linear
│
└── QualityPredictor
├── Linear
├── ReLU
└── Linear
The model contains models.
And those models contain layers.
And those layers contain tensor operations.
This is the pattern for the whole series.
5. The first important question: what does the representation mean?
The most important tensor in the previous example is not necessarily the final score.
It may be this one:
z = self.encoder(context, response)
Why?
Because z is the shared representation from which we can ask more questions.
A scalar predictor throws information away.
The representation can support many predictions.
That immediately suggests the next architecture.
Instead of:
representation
↓
score
we can do:
representation
│
┌────────────┼────────────┐
↓ ↓ ↓
Q head V head policy head
Now our model is no longer answering just one question.
It is producing a small decision surface.
6. Stage two: one representation, multiple heads
Here is a stripped-down version:
class MultiHeadEvaluator(nn.Module):
def __init__(self, embedding_dim=1024, hidden_dim=256, num_actions=3):
super().__init__()
self.encoder = nn.Sequential(
nn.Linear(embedding_dim * 2, hidden_dim),
nn.ReLU(),
nn.LayerNorm(hidden_dim),
nn.Linear(hidden_dim, hidden_dim),
)
self.q_head = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim // 2),
nn.ReLU(),
nn.Linear(hidden_dim // 2, 1),
)
self.v_head = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim // 2),
nn.ReLU(),
nn.Linear(hidden_dim // 2, 1),
)
self.policy_head = nn.Sequential(
nn.Linear(hidden_dim, hidden_dim // 2),
nn.ReLU(),
nn.Linear(hidden_dim // 2, num_actions),
)
The forward pass is still simple:
def forward(self, context, response):
pair = torch.cat([context, response], dim=-1)
z = self.encoder(pair)
q = self.q_head(z).squeeze(-1)
v = self.v_head(z).squeeze(-1)
policy_logits = self.policy_head(z)
return {
"q": q,
"v": v,
"advantage": q - v,
"policy_logits": policy_logits,
}
The architecture sounds more sophisticated.
But recursively decompose it:
MultiHeadEvaluator
│
├── encoder
│
├── Q model
│
├── V model
│
└── policy model
Each head is tiny.
The sophistication comes from what the heads mean together.
That distinction matters.
7. Architecture is often semantics plus composition
A common mistake when reading neural-network code is to look only for exotic layers.
But many meaningful model improvements use ordinary layers differently.
These two networks might both be:
nn.Sequential(
nn.Linear(256, 128),
nn.ReLU(),
nn.Linear(128, 1),
)
One might estimate:
Q(s, a)
while the other estimates:
V(s)
Same primitive architecture.
Different semantic role.
Then:
advantage = Q - V
creates another useful signal without adding a neural layer at all.
This is one of the recurring lessons of the series:
Architectural sophistication is not the same thing as architectural novelty.
Sometimes the breakthrough is a new component.
Sometimes it is a new objective.
Sometimes it is a new relationship between familiar components.
Sometimes it is simply running the same component repeatedly.
8. Stage three: make the heads first-class models
Once Q, V and policy become important enough, it becomes useful to make them explicit modules.
For example:
class QHead(nn.Module):
def __init__(self, input_dim, hidden_dim):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 1),
)
def forward(self, z):
return self.net(z).squeeze(-1)
And:
class VHead(nn.Module):
def __init__(self, input_dim, hidden_dim):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, 1),
)
def forward(self, z):
return self.net(z).squeeze(-1)
And:
class PolicyHead(nn.Module):
def __init__(self, input_dim, hidden_dim, num_actions):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Linear(hidden_dim, num_actions),
)
def forward(self, z):
return self.net(z)
Then composition becomes explicit:
class DecisionModel(nn.Module):
def __init__(self, encoder, q_head, v_head, policy_head):
super().__init__()
self.encoder = encoder
self.q_head = q_head
self.v_head = v_head
self.policy_head = policy_head
def forward(self, context, response):
z = self.encoder(context, response)
q = self.q_head(z)
v = self.v_head(z)
policy_logits = self.policy_head(z)
return {
"q": q,
"v": v,
"advantage": q - v,
"policy_logits": policy_logits,
}
This is worth pausing on.
We have gone from:
one model
to:
model = encoder + Q model + V model + policy model
That is not complication for its own sake.
It gives us independent components that can be:
- trained differently;
- inspected independently;
- frozen independently;
- loaded independently;
- tested independently;
- replaced independently;
- compared independently.
Composition creates experimental freedom.
9. Stage four: what if one forward pass is not enough?
So far every architecture has been fundamentally feed-forward.
We produce a representation and immediately predict from it.
But suppose we want the model to refine internal state.
Then instead of:
input → representation → output
we can think in terms of:
input + previous state → new state
Repeated:
z0
↓
f(input, z0)
↓
z1
↓
f(input, z1)
↓
z2
↓
f(input, z2)
↓
z3
Now the model has something resembling an internal computational trajectory.
Not consciousness.
Not magic reasoning.
A sequence of learned state transitions.
That distinction is important.
10. A recurrent block is still small
A recurrent reasoning block might look like:
class RecurrentBlock(nn.Module):
def __init__(self, input_dim, hidden_dim):
super().__init__()
self.cell = nn.GRUCell(input_dim, hidden_dim)
self.norm = nn.RMSNorm(hidden_dim)
def forward(self, previous_state, current_input):
state = self.cell(current_input, previous_state)
return self.norm(state)
That is it.
The new ability comes from iteration.
For example:
state = torch.zeros(batch_size, hidden_dim, device=x.device)
for _ in range(6):
state = block(state, x)
Now compare that to the feed-forward version:
state = encoder(x)
The primitive block is not dramatically more complicated.
The computation strategy changed.
This is why reading only the layer list can be misleading.
You also need to understand the control flow around the layers.
11. Stage five: two reasoning speeds
Now take recursion one step further.
Instead of one state, maintain two:
low-level state
high-level state
Let the low-level state update several times for every high-level update.
Conceptually:
input
│
▼
projected representation
│
├──────────────────────────┐
│ │
▼ │
low-level state │
│ │
├─ update │
├─ update │
├─ update │
└─ update │
│ │
▼ │
high-level state ────────────┘
In pseudocode:
for cycle in range(num_cycles):
for step in range(low_level_steps):
low_state = low_block(
low_state,
torch.cat([x, high_state], dim=-1),
)
high_state = high_block(
high_state,
torch.cat([low_state, high_state], dim=-1),
)
That sounds like a “hierarchical reasoning model”.
And it is.
But look at what it is made from:
2 recurrent blocks
+
2 state tensors
+
a nested loop
Again:
The model becomes understandable when we decompose it.
12. The prediction surface can evolve independently of the reasoning core
Once we have a richer internal representation, we can ask more questions of it.
For example:
hidden state
│
┌───────────┬───────┼─────────┬───────────┐
↓ ↓ ↓ ↓ ↓
score uncertainty OOD consistency reconstruction
A model can therefore evolve along at least two dimensions:
How does it compute a representation?
and:
What predictions does it make from that representation?
Those are different architectural decisions.
For example, a score head might be:
self.score_head = nn.Linear(hidden_dim, 1)
An uncertainty head might also be:
self.logvar_head = nn.Linear(hidden_dim, 1)
An OOD head:
self.ood_head = nn.Linear(hidden_dim, 1)
Three identical layer shapes.
Three completely different meanings.
Again, semantics matter.
13. Uncertainty is a model output too
A scalar quality score gives us:
0.82
But it does not tell us whether the model thinks:
I am very sure this is 0.82
or:
I barely know what I am looking at, but 0.82 is my best guess
One approach is to learn a second quantity representing uncertainty.
For example:
score_logit = self.score_head(z)
log_variance = self.logvar_head(z)
The architecture has not suddenly become enormous.
We added another tiny head.
But the information surface became much richer.
This pattern will appear repeatedly.
14. Calibration is another small model
Suppose our score logits are systematically overconfident.
We might learn a temperature:
temperature_raw = self.temperature_head(z)
temperature = 0.5 + 0.5 * torch.nn.functional.softplus(temperature_raw)
score = torch.sigmoid(score_logit / temperature)
Now the model contains a learned calibration mechanism.
Again, recursively:
calibration system
↓
Linear(hidden_dim, 1)
↓
softplus
↓
scale logits
The label sounds large.
The implementation may be tiny.
15. Reconstruction turns representation quality into something measurable
Another useful trick is to ask the hidden state to reconstruct part of the input representation.
For example:
self.reconstruction_head = nn.Linear(hidden_dim, hidden_dim)
Then:
reconstructed = self.reconstruction_head(z)
Compare it to the original projected input:
similarity = torch.nn.functional.cosine_similarity(
reconstructed,
projected_input,
dim=-1,
)
Now the model is not only asked:
What is the score?
It is also asked:
Did the representation preserve enough information about the thing being judged?
The diagnostic head becomes a probe into the model’s internal representation.
16. Stage six: simplify the recursion
Hierarchical recurrence is useful, but perhaps we can get much of the benefit from a smaller recursive state machine.
Imagine three vectors:
x = goal
y = response
z = current latent state
Fuse them:
fused = torch.cat([x, y, z], dim=-1)
Project:
z_next = torch.tanh(self.z_projection(fused))
Process:
z_next = self.core(z_next)
Then make a small residual update:
z = z + 0.1 * z_next
Repeat.
for _ in range(num_recursions):
fused = torch.cat([x, y, z], dim=-1)
z_next = torch.tanh(self.z_projection(fused))
z_next = self.core(z_next)
z = z + step_scale * z_next
That is a recursive model.
The model is repeatedly reconsidering the same goal/response pair while carrying forward a learned latent state.
Still no magic.
Just tensors and a loop.
17. What is the core inside that recursive model?
Maybe this:
class TinyBlock(nn.Module):
def __init__(self, d_model, dropout=0.1):
super().__init__()
self.norm = nn.LayerNorm(d_model)
self.mlp = nn.Sequential(
nn.Linear(d_model, d_model * 4),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(d_model * 4, d_model),
nn.Dropout(dropout),
)
def forward(self, x):
return x + self.mlp(self.norm(x))
That should look familiar if you completed the PyTorch series.
It is just:
LayerNorm
↓
Linear expansion
↓
GELU
↓
Linear projection
↓
residual
The recursive model is made from residual blocks.
The residual blocks are made from familiar PyTorch layers.
That is exactly why we did the first series first.
18. Add attention without changing the idea
We can replace the core with an attention-enhanced block:
class TinyAttentionBlock(nn.Module):
def __init__(self, d_model, num_heads=4, dropout=0.1):
super().__init__()
self.norm = nn.LayerNorm(d_model)
self.attention = nn.MultiheadAttention(
embed_dim=d_model,
num_heads=num_heads,
dropout=dropout,
batch_first=True,
)
self.ff = TinyBlock(d_model, dropout)
def forward(self, x):
h = self.norm(x)
attended, _ = self.attention(h, h, h, need_weights=False)
x = x + attended
return self.ff(x)
Now the core can model interactions across a sequence.
But the outer recursion is unchanged.
This is another powerful architectural principle:
If components have clean interfaces, you can replace the inside without rewriting the outside.
19. A sparse autoencoder is another model inside the model
Suppose the final latent state is dense:
z = [0.71, -0.13, 0.42, 0.09, ...]
We might want a smaller sparse concept representation.
One simple approach is a sparse autoencoder-style bottleneck:
class SparseBottleneck(nn.Module):
def __init__(self, d_model):
super().__init__()
self.encoder = nn.Sequential(
nn.Linear(d_model, d_model // 2),
nn.ReLU(),
nn.LayerNorm(d_model // 2),
)
self.decoder = nn.Linear(d_model // 2, d_model)
def forward(self, z):
concepts = self.encoder(z)
reconstruction = self.decoder(concepts)
return concepts, reconstruction
Now our recursive model contains:
recursive state model
↓
core neural blocks
↓
sparse autoencoder
↓
prediction heads
A model inside a model inside a model.
And every layer is still something we already know how to inspect.
20. Stage seven: go below the model
Eventually we reach another layer of the stack.
So far we have asked:
What architecture should compute the prediction?
But the model also has to learn.
That means parameter updates.
The default answer is often AdamW.
But optimizers are code too.
They can be decomposed too.
Suppose we maintain two statistics for every parameter:
moving average of gradients
moving average of squared gradients
In simplified form:
grad_avg = beta * grad_avg + (1 - beta) * grad
precond = decay * precond + (1 - decay) * grad.square()
Then normalize the averaged gradient:
step = grad_avg / (precond.sqrt() + eps)
And update:
parameter -= learning_rate * step
That is already the heart of an adaptive, preconditioned optimizer.
The optimizer sounded like a completely different kind of thing.
But the same first-principles method works:
optimizer
↓
state variables
↓
update equations
↓
tensor operations
Nothing gets a black-box exemption.
21. The complete evolutionary picture
Now step back.
The progression looks something like this:
1. Pair scorer
context + response
↓
representation
↓
score
Then:
2. Multi-head evaluator
context + response
↓
representation
↓
┌─────┼─────┐
Q V Policy
Then:
3. Explicit compositional model
encoder
+
Q model
+
V model
+
policy model
Then:
4. Recurrent reasoning
input + state
↓
state update
↓
repeat
Then:
5. Hierarchical recurrence
fast state updates
↓
slow state update
↓
repeat
Then:
6. Compact recursive model
x + y + z
↓
small core
↓
residual state update
↓
repeat
Then:
7. Rich diagnostics
latent state
↓
score / uncertainty / OOD / consistency / calibration / reconstruction
Then:
8. Interpretable bottleneck
latent state
↓
sparse concepts
↓
reconstruction
Then:
9. Learning machinery
gradient
↓
moving statistics
↓
preconditioned step
↓
parameter update
At no point did we need to jump from “simple neural network” to “incomprehensible intelligence machine”.
We added capabilities one architectural decision at a time.
22. This is how I want you to read model code
When you open an unfamiliar PyTorch model, do not start by reading every line.
First ask:
What are the inputs?
For example:
context embedding
response embedding
latent state
sequence length
What are the persistent states?
For example:
parameters
recurrent state
optimizer state
What is the main representation?
Look for tensors like:
z
hidden
state
features
encoded
What transforms that representation?
Look for:
MLP
attention
GRU
convolution
projection
normalization
Is computation repeated?
Look for:
for step in range(...):
or recurrent calls.
What outputs come from the representation?
Look for heads:
score_head
q_head
v_head
policy_head
uncertainty_head
ood_head
What objectives train those outputs?
An output head without its loss tells you only half the architecture.
What is diagnostic rather than primary?
Not every output is the main decision.
Some exist to regularize or inspect the representation.
How do parameters move?
Finally inspect:
optimizer
learning rate
weight decay
gradient clipping
schedulers
custom update rules
That sequence is much easier than reading top-to-bottom.
23. Draw the model before you explain it
For every post in this series, we are going to draw the architecture as a data-flow graph.
Not because diagrams are decorative.
Because diagrams expose misunderstanding.
If you cannot reduce a model to something like:
input
↓
encoder
↓
latent state
↓
heads
then you probably do not understand it yet.
For recurrent models, the arrows should show recurrence.
For hierarchical models, the arrows should show update rates.
For multi-head models, the shared representation should be obvious.
For optimizers, the state variables should be visible.
The diagram becomes a test of our explanation.
24. Then prove the diagram with tensor shapes
Suppose the architecture says:
context [B, 1024]
response [B, 1024]
↓ concatenate
pair [B, 2048]
↓ encoder
z [B, 256]
↓ Q head
q [B]
The code should prove exactly that.
B = 8
context = torch.randn(B, 1024)
response = torch.randn(B, 1024)
pair = torch.cat([context, response], dim=-1)
assert pair.shape == (B, 2048)
z = encoder(pair)
assert z.shape == (B, 256)
q = q_head(z)
assert q.shape == (B,)
This is where the previous debugging-heavy PyTorch series pays off.
The architecture diagram is a hypothesis.
The runtime tensor shapes are evidence.
25. Parameter counts tell another part of the story
A model can sound sophisticated while still being tiny.
Always count parameters.
def parameter_count(model):
return sum(p.numel() for p in model.parameters())
And by component:
def parameter_report(model):
for name, module in model.named_children():
count = sum(p.numel() for p in module.parameters())
print(f"{name:24s} {count:12,d}")
A multi-head architecture might reveal:
encoder 590,000
q_head 33,000
v_head 33,000
policy_head 33,500
The conceptual richness may come mostly from adding several very cheap heads to one expensive representation.
That matters when designing real systems.
26. Activation counts matter too
Parameters are not the whole cost.
A recurrent model may reuse the same parameters several times.
For example:
for _ in range(8):
z = block(z)
The parameter count did not increase eightfold.
The compute did.
Likewise, attention may use a modest number of parameters but expensive activations as sequence length grows.
So throughout the series we will separate:
parameter complexity
compute complexity
activation memory
training-state memory
These are not interchangeable.
27. A recurring experiment: remove the new idea
Whenever a model introduces something new, one of the best ways to understand it is to remove it.
If we add a V head:
What changes if we train without V?
If we add recurrence:
What happens with one recursion instead of six?
If we add hierarchy:
What happens if low and high state update at the same rate?
If we add a sparse bottleneck:
What changes when the bottleneck is bypassed?
If we add a custom optimizer:
What changes against AdamW under the same budget?
That is an ablation.
And it turns architecture discussion into evidence.
28. Another recurring experiment: replace the component
Composition also lets us ask:
What if this component were different?
For example:
MLP core
↓ replace with
attention core
Or:
GRUCell
↓ replace with
simple MLP state update
Or:
learned temperature
↓ replace with
fixed temperature
Or:
custom optimizer
↓ compare with
AdamW
The interface remains fixed while the implementation changes.
That is one reason modular architecture is powerful.
29. Small models are useful precisely because we can inspect them
There is another reason for this series.
Large language models dominate AI discussion, but many useful decisions do not require a large generative model.
Examples include:
ranking
quality scoring
classification
routing
uncertainty estimation
out-of-distribution detection
policy selection
preference prediction
calibration
A small model has several advantages:
- cheap inference;
- local execution;
- deterministic latency;
- easier retraining;
- clearer inputs and outputs;
- easier ablation;
- easier instrumentation;
- easier failure analysis.
This series is not arguing that small models replace LLMs.
It is arguing that not every learned decision should automatically become an LLM call.
30. LLMs can generate the code. You still need the model in your head.
This connects directly to the debugging philosophy of the previous series.
An LLM can generate:
class RecurrentEvaluator(nn.Module):
...
It can generate the heads.
It can generate the optimizer.
It can generate a training loop.
What you still need to know is:
What is this model claiming to represent?
Which tensor carries that representation?
Which outputs depend on it?
Which loss teaches each output?
What is recurrent?
What is shared?
What is merely diagnostic?
What is actually being optimized?
Generated code without an architectural model in your head is just a larger debugging surface.
First principles give you that architectural model.
31. The series roadmap
Here is the working plan.
Step 00 — The Model Inside the Model
This article.
How to recursively decompose neural architectures and how the model lineage we are going to study evolves.
Step 01 — MR.Q: Building a Neural Quality Model From Two Embeddings
We will build the smallest useful evaluator:
context + response → representation → Q value
We will inspect every tensor and loss.
Step 02 — Inside MR.Q: Encoders, Predictors and Pair Representations
We zoom into the apparently simple model and treat the encoder and predictor as independent learned systems.
We will test alternative pair representations and show exactly what information is lost or preserved.
Step 03 — EBT: From One Score to Q, V, Policy and Advantage
We turn one prediction into a multi-head decision surface.
We will explain why Q and V are different, why their difference matters, and what a policy head contributes.
Step 04 — SICQL: Building a Decision Model From Smaller Models
We make Q, V and policy explicit modules and explore independent training, loading, replacement and inspection.
Step 05 — HRM: Hierarchical Reasoning With Fast and Slow Recurrent State
We introduce recurrence and then hierarchy.
The focus will be state evolution, not mystique.
Step 06 — Inside HRM: GRUs, RMSNorm, Calibration and Diagnostic Heads
We decompose the hierarchy into its smallest reusable components and examine uncertainty, OOD, consistency and reconstruction heads.
Step 07 — Tiny: Recursive Reasoning With a Small Neural Network
We simplify hierarchical recurrence into a compact latent-state loop.
Step 08 — Inside Tiny: Residual Blocks, Attention and Sparse Autoencoders
We open the recursive core and then open the model inside that model: the sparse bottleneck.
Step 09 — PACS: Building an Optimizer From Gradient Statistics
We move beneath architecture and implement the learning rule itself.
Step 10 — What Each Architecture Adds
We compare the lineage under the same conceptual task and ask what we actually gain at each step.
Not by name.
By capability, cost and evidence.
32. One important rule for the whole series
We will not use architecture names as explanations.
This sentence is not enough:
The model uses hierarchical recurrence.
We need:
what state exists;
how it is initialized;
what enters the update;
how often it updates;
what parameters are shared;
what tensor leaves the loop;
what losses act on that tensor.
Likewise:
The model has uncertainty estimation.
is not enough.
We need to find:
which head predicts it;
what the output means;
what target trains it;
how it affects the final decision;
how it is calibrated;
how we know it works.
Names are useful handles.
They are not explanations.
33. Another rule: separate architecture from training
A PyTorch class tells us the forward computation.
It does not automatically tell us how the model learns.
For every architecture we will distinguish:
ARCHITECTURE
what computes what
from:
OBJECTIVE
what losses define success
from:
OPTIMIZATION
how gradients change parameters
from:
DATA
what examples teach the model
Those four things are often blurred together in model discussions.
We will keep them separate.
34. And another rule: outputs are not evidence that they mean what their names say
If I write:
self.uncertainty_head = nn.Linear(hidden_dim, 1)
I have not created uncertainty estimation.
I have created a scalar output called uncertainty_head.
For it to mean uncertainty, I need:
- an appropriate target or objective;
- training data that contains relevant information;
- validation showing the output tracks uncertainty;
- calibration tests;
- failure analysis.
The same applies to:
reasoning
agreement
consistency
OOD
quality
preference
confidence
Model names are hypotheses.
Evaluation decides whether the hypotheses survived contact with data.
That scientific boundary will matter throughout the series.
35. Start from the tensor, not the branding
Here is a useful habit.
If someone tells you:
This is a hierarchical recursive uncertainty-aware reasoning model.
translate it immediately into questions:
What tensor goes in?
What tensor represents state?
What operation updates it?
How many times?
Where is the hierarchy?
Where is uncertainty produced?
What target trains uncertainty?
What makes the process reasoning rather than repeated transformation?
The point is not cynicism.
The point is precision.
Good architecture survives decomposition.
36. The final mental model
If you remember one picture from this introduction, make it this one:
MODEL
│
├── representation model
│ │
│ ├── projection
│ ├── normalization
│ └── learned transforms
│
├── reasoning/state model
│ │
│ ├── recurrent update
│ ├── attention
│ └── residual processing
│
├── prediction models
│ │
│ ├── score
│ ├── value
│ ├── policy
│ ├── uncertainty
│ └── diagnostics
│
└── learning system
│
├── losses
├── gradients
├── optimizer state
└── parameter updates
Every box can be opened.
Every arrow can be inspected.
Every tensor can be printed.
Every claim can be tested.
That is what Models From First Principles means.
Next: build the smallest useful model
In Step 01, we will build MR.Q from scratch.
Not as a named architecture to memorize.
As a problem:
Given a context and a candidate response, can a small neural model learn a useful scalar quality estimate?
We will start with two embeddings and build upward:
context embedding
+
response embedding
↓
pair representation
↓
encoder
↓
Q predictor
↓
scalar estimate
We will inspect:
- what the encoder is actually learning;
- whether concatenation is enough;
- logits versus probabilities;
- regression versus ranking objectives;
- parameter counts;
- gradient flow;
- overfitting a tiny dataset;
- calibration;
- failure cases;
- and how to prove the model is learning the relationship we think it is learning.
The model will be small.
That is the point.
We are going to understand all of it.