AIBussin Book Published

PyTorch From First Principles

Understand PyTorch from first principles — from tensor geometry and autograd to model structure, transforms, attention, debugging, performance, compilation, reproducibility, and a GPT-style model built from scratch.

PyTorch is remarkably easy to start using before it is easy to understand.

You can create a tensor, copy a model architecture, call loss.backward(), construct an optimizer, and have a training loop running in a few minutes.

That is one of PyTorch’s strengths.

It is also where the trouble begins.

A program can run while broadcasting the wrong dimensions. A parameter can have a gradient and still never be updated. A model can train successfully while consuming inputs whose numerical meaning is wrong. A GPU can appear underutilized even though adding workers makes the program slower. A checkpoint can load without error while the model behaves differently because its preprocessing changed. torch.compile can appear to work while quietly producing more graphs than you expected.

None of those problems are solved by knowing one more API call.

They are solved by knowing what computation PyTorch is actually performing.

That is what this book is about.

Make hidden structure visible before guessing.

We will build PyTorch upward from its smallest mechanisms and repeatedly ask the same kind of question:

What does PyTorch think is happening here, what actually happened, and what evidence would distinguish the two?

The goal is not to memorize PyTorch.

The goal is to make unfamiliar PyTorch code inspectable.


The system we are going to build

The book begins with almost nothing:

one value
    ↓
one parameter
    ↓
one prediction
    ↓
one loss
    ↓
one gradient
    ↓
one update

By the end, those same mechanisms will have expanded into a small GPT-style language model:

raw data
    ↓
Dataset / transforms / DataLoader
    ↓
token tensors
    ↓
embeddings
    ↓
attention
    ↓
residual blocks
    ↓
logits
    ↓
cross entropy
    ↓
autograd
    ↓
optimizer
    ↓
updated parameters
    ↓
checkpoint / generation / evaluation

Around that model sits another system:

CPU work
GPU work
memory
profiling
compilation
reproducibility
measurement
regression testing

Those are not separate subjects.

They are different views of the same running program.

The book builds enough of that program from first principles that when something goes wrong, you can identify which part of the system to interrogate.


The recurring debugging move

A lot of PyTorch debugging begins too late.

A tensor reaches an operation with the wrong shape, so we reshape it until the exception disappears.

A parameter has grad=None, so we add requires_grad=True.

GPU utilization is low, so we increase num_workers.

Training is unstable, so we lower the learning rate.

Compilation is slow, so we disable torch.compile.

Sometimes those changes work.

But a working change is not automatically a diagnosis.

This book uses a different pattern:

predict
    ↓
observe
    ↓
find the first divergence
    ↓
identify the mechanism
    ↓
change one thing
    ↓
verify the repair

The chapters gradually turn that pattern into a collection of concrete diagnostic techniques.

For tensors:

Find where the tensor first became wrong, not merely where the wrong tensor finally became illegal.

For autograd:

Find the first point where the gradient path stops existing.

For modules and parameters:

Inspect the structure PyTorch sees rather than inferring it from the source code.

For input pipelines:

Find where useful work stops flowing between producer and consumer.

For transforms:

Find the first boundary where the sample stops satisfying the representation the model expects.

The particular mechanism changes.

The investigation does not.


PyTorch contains several overlapping structures

One reason PyTorch can become confusing is that several different systems occupy the same Python program.

Consider a model with a few tensors and submodules.

There is the ordinary Python object structure:

model
 ├── encoder
 ├── blocks
 └── head

There is the structure PyTorch has registered:

_modules
_parameters
_buffers

There is the computation graph created by the operations executed during the current forward pass:

parameter
    ↓
operation
    ↓
intermediate
    ↓
loss

There are optimizer parameter groups:

optimizer
    ↓
specific parameter objects

There is the input pipeline:

source
  ↓
Dataset
  ↓
transforms
  ↓
collation
  ↓
DataLoader workers
  ↓
batch

And there is the execution system underneath all of it:

Python
CPU threads
worker processes
host memory
CUDA
GPU memory
compiled graphs

A tensor can belong to one of these systems without belonging to another.

A parameter can participate in autograd without being registered in the model.

A registered parameter can exist without being owned by the optimizer you created earlier.

A transform can return a tensor with the correct shape and dtype while changing what its values mean.

A batch can arrive quickly and still be wrong.

A GPU can be busy while the program as a whole makes little progress.

Much of PyTorch debugging becomes easier once you stop treating all of these structures as one thing.


What this book is designed to teach

The book progresses from small mechanisms to complete systems.

You will learn to reason about:

  • tensor geometry — shape, dtype, device, stride, broadcasting, contiguity, layout, and the meaning attached to each axis;
  • autograd — what PyTorch records, leaves and intermediates, grad_fn, gradient accumulation, broken paths, retained gradients, and graph lifetime;
  • neural networks from raw tensors — parameters, matrix multiplication, nonlinearities, logits, loss, gradients, and manual updates before nn.Module hides the bookkeeping;
  • registered model structure — modules, parameters, buffers, recursive composition, state_dict(), device movement, train/eval behavior, optimizer membership, and the places where those structures can disagree;
  • input delivery — datasets, collation, multiprocessing, workers, prefetching, persistent state, host-to-device transfer, and the difference between a pipeline that waits and one that merely moves the bottleneck;
  • input representation — transforms, scaling, normalization, augmentation, target semantics, pretrained preprocessing contracts, and the difference between a legal tensor and a meaningful one;
  • spatial models — channels, height, width, convolution, receptive geometry, and deriving CNN shapes rather than guessing them;
  • higher-dimensional representations — features, embeddings, attention, heads, masks, logits, and transformer-style tensor geometry;
  • training failures — data, targets, losses, gradients, optimizers, parameter movement, numerical instability, and models that run while refusing to learn;
  • execution and performance — CUDA timing, memory, profiling, bottlenecks, compilation, graph breaks, guards, recompilation, and dynamic behavior;
  • experiments — baselines, reproducibility, comparable runs, regressions, benchmark validity, and turning discovered failures into tests; and
  • integration — assembling the mechanisms into a small GPT-style language model whose behavior can be inspected from input through generation.

The important part is not the size of that list.

It is that the topics accumulate.

Later chapters should feel like larger applications of structures you already understand.


One system, built progressively

The sequence is deliberate.

We begin with the training loop because almost everything else in PyTorch eventually has to attach to it.

Then we expose the tensor flowing through that loop.

Then we expose the graph connecting the tensor operations.

Then we build a neural network without the machinery that normally organizes it.

Only after those pieces are visible do we introduce nn.Module and ask what PyTorch thinks belongs to the model.

Then we move outside the model.

We ask how the next batch reaches it.

Then we ask what that batch actually means.

Only after the input contract is trustworthy do we start exploiting spatial structure with convolution and increasingly complex model architectures.

Later, once all the pieces exist, the book becomes more forensic.

We ask questions like:

Why does this model run but not learn?

Where is GPU memory actually going?

Why is the GPU waiting?

Which part of this operation is asynchronous?

Why did this compiled function produce another graph?

Which input property created that guard?

Did this optimization improve throughput,
or did the measurement merely move?

Is this new result genuinely worse,
or are the two experiments not comparable?

And finally we assemble the pieces.

By then the capstone should not feel like twenty unrelated PyTorch tricks combined into a transformer.

Each mechanism should already have somewhere to attach.


This distinction appears again and again throughout the book.

PyTorch is very good at rejecting computations that are impossible.

It cannot reject every computation that is meaningless.

A broadcast may be legal while pairing the wrong axes.

A floating-point image may be perfectly valid while still representing values in 0..255 when the model expects 0..1.

A horizontal flip may execute perfectly while changing the truth of the label.

A parameter may receive gradients while remaining outside the optimizer.

A checkpoint may load with every key matched while inference preprocessing no longer matches training.

A benchmark may run correctly while comparing two different workloads.

So we will repeatedly separate:

CAN PYTORCH EXECUTE THIS?

from

IS THIS THE COMPUTATION WE INTENDED?

Error messages help with the first question.

This book is primarily about learning to answer the second.


Observation before explanation

Another rule follows naturally:

Inspect what happened before explaining why it happened.

Before diagnosing a tensor problem, print the tensor.

Before diagnosing a gradient problem, inspect the graph and the gradients.

Before diagnosing an optimizer problem, check whether the parameter object is actually in the optimizer and whether it moved.

Before diagnosing an input problem, trace one known sample through the transforms.

Before diagnosing DataLoader performance, measure batch wait and consumer work separately.

Before diagnosing CUDA performance, distinguish host time from device time.

Before diagnosing compilation, inspect the actual graph breaks and recompilation reasons.

Before diagnosing a regression, establish that the two runs are comparable.

An explanation that arrives before the evidence is a hypothesis.

Treat it as one.


Using AI without outsourcing understanding

Modern PyTorch development increasingly involves AI-generated code.

That is not a problem this book tries to avoid.

It is one of the reasons the book matters.

An assistant can often produce a working training loop, a custom dataset, an attention block, a profiler script or a torch.compile configuration much faster than you would write one from scratch.

The question is what happens when the generated program behaves strangely.

If the only thing you know how to do is ask for another implementation, then every failure becomes another generation problem.

If you understand the underlying mechanisms, you can ask much better questions:

Show me the tensor shapes after every operation.

Which exact edge disconnects this parameter from the loss?

Which registered parameters are absent from the optimizer?

What does this transform assume its input range is?

Where is the DataLoader consumer actually waiting?

Which graph break caused this function to recompile?

What observation would distinguish these two explanations?

AI can generate the implementation.

You still need enough understanding to know what evidence to request when the implementation stops making sense.

That is the relationship with AI this book assumes.


What you should be able to do after reading

The goal is not that you remember every PyTorch API used in these chapters.

The goal is that an unfamiliar PyTorch program becomes legible.

You should be able to open one and ask:

What does this tensor represent?

What should its shape be?

What does each dimension mean?

Where was this tensor created?

What operations produced this value?

Does the loss depend on this parameter?

Where does that dependency first disappear?

Which tensors does PyTorch consider parameters?

Which state is registered?

Which exact parameter objects does the optimizer own?

Did those objects move after the step?

What happened to this sample before the model received it?

Does the transformed sample still mean what its target says it means?

Where does the next batch spend its time?

Is the CPU waiting, computing, or contending?

Is this CUDA timing measuring submission or completed work?

Where is memory allocated?

Why did compilation create another graph?

What assumption produced that guard?

Did this intervention actually improve throughput or accuracy?

Are these two experiments comparable?

Those questions are more useful than knowing where a particular method sits in the documentation.

They tell you how to investigate.


You will also learn when not to use more PyTorch

Not every problem needs another abstraction.

You do not always need:

  • a more complicated model;
  • another layer;
  • mixed precision;
  • additional workers;
  • pinned memory;
  • compilation;
  • a custom CUDA kernel;
  • a more elaborate optimizer;
  • another augmentation;
  • another framework around PyTorch.

Sometimes the tensor is simply wrong.

Sometimes the parameter is not in the optimizer.

Sometimes the dataset is duplicated.

Sometimes the validation pipeline differs from the training contract.

Sometimes the GPU is not the bottleneck.

Sometimes a plain eager implementation is already fast enough.

The book therefore tries to earn complexity.

We add machinery when the simpler system exposes a limitation that the new mechanism actually solves.


Who this book is for

You should be comfortable with basic Python:

variables
functions
loops
classes
lists
dictionaries
running scripts

You do not need previous PyTorch experience.

You do not need to know transformer internals before beginning.

You do not need to arrive with a complete mathematical treatment of neural networks.

You do need to be willing to run small experiments.

Many chapters deliberately break working code.

That is not an interruption to the lesson.

It is the lesson.

The book is written for programmers who want to be able to investigate PyTorch rather than merely operate it when everything goes according to plan.


What this book does not try to cover

This is not an encyclopedia of every PyTorch API.

It does not attempt to make every specialization a prerequisite for competence.

Topics such as:

  • large-scale distributed training;
  • multi-node infrastructure;
  • custom CUDA kernels;
  • quantization;
  • production serving;
  • LoRA and other parameter-efficient fine-tuning systems;
  • mixture-of-experts architectures;
  • every current transformer variant;
  • every vision architecture;
  • every compiler backend;

are important.

But they are not the foundation.

The purpose of this book is to give those subjects somewhere to attach.

If tensors, computation graphs, registered state, input contracts, optimization, execution, measurement and debugging are clear, unfamiliar features stop arriving as isolated incantations.

They become extensions of a system you already understand.


The promise

By the end of PyTorch From First Principles, you should be able to encounter PyTorch code you have never seen before and work out:

what tensors mean, how they were produced, what PyTorch has recorded about them, what state belongs to the model, what the optimizer can actually change, what representation reaches the model, where execution time and memory are going, and what evidence distinguishes a real repair from a plausible-looking change.

More compactly:

Make the hidden structure visible. Find the first divergence. Repair the mechanism that caused it. Verify that the system actually improved.

That is the standard the rest of the book is built around.

The final GPT-style model matters.

But it is not the real destination.

The real destination is reaching the point where, when a model does something you did not expect, you know how to find out why.

Contents

Chapters

01

What Are We Actually Doing?

Build the smallest PyTorch training loop from one scalar parameter, derive the gradient by hand, separate forward, backward, and update, then break gradient clearing and parameter identity to learn how to diagnose training from evidence instead of loss curves.

02

The Tensor: What Is Actually Flowing Through the Loop?

Make tensor behavior inspectable by deriving shapes before execution, naming axis semantics, tracing broadcasting, reductions, reshapes, strides, dtype, and device, then locate the first tensor whose observed structure diverges from the contract.

04

The Network: What Is It Without nn.Module?

Build a two-layer neural network entirely from tensors—weights, nonlinearities, logits, cross-entropy, initialization, gradients, and manual updates—then use a six-claim audit to separate representation, computation, objective, differentiation, update, and evaluation failures.

05

nn.Module: What Does PyTorch Think Belongs to Your Model?

Expose how nn.Module organizes registered parameters, buffers, submodules, state_dict(), device movement, and optimizer membership, then reconcile the Python object graph, registered tree, autograd graph, and optimizer groups when model state silently diverges.

06

DataLoader: Where Is the Training Loop Actually Waiting?

Model DataLoader as a producer-consumer system, measure batch wait separately from training work, and use controlled worker, prefetch, persistence, and contention experiments to find the resource that actually limits input throughput.

07

Transforms: What Does the Model Actually See?

Trace one known sample through preprocessing to verify structure, numeric interpretation, semantics, relationships, and provenance, then diagnose legal-but-wrong transforms, scaling, normalization, augmentation, and target alignment at the first broken representation boundary.

08

CNN Geometry: What Shape Reaches the Next Layer?

Derive CNN channel and spatial geometry before execution, build convolutions and pooling from first principles, trace receptive fields and flatten boundaries, and use a shape ledger to find the first layer where predicted and observed geometry diverge.

09

Feature Space: What Does a Linear Model Actually See?

Learn what a vector in feature space represents by separating tensor rank from representation dimensionality, deriving dot products, cosine similarity, distance, and linear scores, then test invariances and recover a planted separating direction with a simple linear model.

10

Attention: Which Position Is Comparing With Which?

Build multi-head attention from tensor geometry, name every query, key, head, and position axis, verify masks, softmax, scaling, and head round-trips, then use four levels of evidence—from shape to behavioral intervention—to locate the first semantic failure.

11

Training: Which Link in the Learning Chain Is Broken?

Diagnose models that run but do not learn by walking TASK → OBJECTIVE → DEPENDENCY → GRADIENT → OPTIMIZER OWNERSHIP → PARAMETER UPDATE → CAPABILITY, using one fixed batch, exact parameter identity, controlled SGD, and tiny-batch overfitting to find the first missing consequence of learning.

12

Performance: What Is the Machine Waiting For?

Turn PyTorch performance into a measured comparison by defining the workload and metric, correcting CUDA timing, separating training phases, tracing allocated and reserved memory, profiling bottlenecks, and testing AMP, checkpointing, SDPA, and torch.compile as evidence-backed interventions.

13

Compilation: Which Assumption Stopped Holding?

Treat torch.compile as a runtime contract: distinguish correctness from capture, stability, and value, inspect graph breaks, guards, cached variants, dynamic shapes, recompilation, and cold versus warm cost, then find the first compiler assumption that stopped holding.

14

Regressions: Did the Model Change, or the Measurement?

Establish whether a regression exists before explaining it by measuring baseline variation, using paired comparisons when valid, fingerprinting the evaluation path, controlling randomness and determinism, bisecting interacting changes, and recording runs so model changes can be separated from measurement changes.

15

Assembly: A Language Model You Can Interrogate

Assemble a small GPT-style language model and interrogate it with the book's full diagnostic stack—tokenizer and target contracts, attention interventions, parameter accounting, learning and performance ledgers, compilation, generation, and paired evaluation—so the finished model remains inspectable rather than magical.

16

Appendix A: PyTorch Diagnostic Field Guide

Route real PyTorch symptoms to the smallest diagnostic instrument that can resolve them, with field tables, evidence packets, incident worksheets, AI prompts, escalation rules, and the complete stack from tensor tracing through regressions and compiled execution.