LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnMath & StatisticsVectors, Matrices & Tensors
🏛️EasyModel Architecture

Vectors, Matrices & Tensors

Trace one latency prediction into matrix products and token tensors. Check shapes, distinguish dot products from cosine similarity, and catch silent axis mistakes.

18 min read
Learning path
Step 11 of 196 in the full curriculum
Gradients and BackpropLinear Algebra for ML

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

The gradients lesson computed [4, 2] for a CodeAssist latency prediction: one loss slope for the prompt-length weight and one for the queue-delay weight. That ordered list has a name. It's a vector. The gradient tells us how loss changes; the learning rate turns it into a weight update.

What changes when you process three requests together, or keep a separate vector for every token in a prompt? The arithmetic stays familiar, but extra axes answer a different question: which request, which token, and which feature does each number belong to? Once you see how one latency prediction turns into a row-column dot product, stacking multiple requests into a matrix and entire prompt batches into a 3D tensor becomes second nature.

Start with the vector you already computed

Before stacking anything, name the object in front of you. One number is a scalar, such as a loss of 1.0. An ordered list of numbers is a vector. A rectangular table of numbers is a matrix. The numbers don't carry their own meaning; axis labels and position conventions do.

Reuse the two-weight latency row:

VectorMeaning of position 0Meaning of position 1Values
featuresprompt length in hundreds of tokensqueue delay in seconds[2, 1]
weightsseconds per prompt blockseconds per second of queue delay[2, 2]
gradientloss slope with respect to the prompt-length weightloss slope with respect to the queue-delay weight[4, 2]

Each vector above has length 2, but they play distinct physical roles. Shape tells you how values sit in memory; names and coordinates tell you what they actually mean.

The NumPy lesson introduced .shape. A shape of (2,) denotes a single axis containing two items. That trailing comma is Python's syntax for a one-element tuple so it isn't confused with grouped parentheses. A raw scalar has shape () because it has zero axes.

Use NumPy arrays throughout this lesson; ordinary Python lists don't implement @. This first example repeats the learning-rate update with arrays. All three inputs should have shape (2,), and subtracting 0.1 * gradient should give [1.6, 1.8]:

a-gradient-is-a-vector.py
1import numpy as np 2 3features = np.array([2.0, 1.0]) 4weights = np.array([2.0, 2.0]) 5gradient = np.array([4.0, 2.0]) 6updated = weights - 0.1 * gradient 7 8print("features", features.tolist(), "shape", features.shape) 9print("weights", weights.tolist(), "shape", weights.shape) 10print("gradient", gradient.tolist(), "shape", gradient.shape) 11print("updated_weights", updated.tolist())
Output
1features [2.0, 1.0] shape (2,) 2weights [2.0, 2.0] shape (2,) 3gradient [4.0, 2.0] shape (2,) 4updated_weights [1.6, 1.8]

A vector isn't automatically an embedding. Here it holds two named features or two parameters. Later, a token embedding is a longer vector whose positions are learned through training rather than labeled by hand.

Those updated weights form a fresh vector. How does that weight vector turn into a concrete latency prediction?

Let aligned features make one prediction

The latency model uses a dot product: multiply matching positions, then add. Let xxx name the features, www the weights, and y^\hat{y}y^​ ("y hat") the predicted latency. Subscripts 1 and 2 below identify the two coordinates; Python indexes those same positions with 0 and 1.

y^=x⋅w=x1w1+x2w2\hat{y} = x \cdot w = x_1w_1 + x_2w_2y^​=x⋅w=x1​w1​+x2​w2​

With the last chapter's starting weights [2, 2]:

[2,1]⋅[2,2]=(2×2)+(1×2)=6[2, 1] \cdot [2, 2] = (2 \times 2) + (1 \times 2) = 6[2,1]⋅[2,2]=(2×2)+(1×2)=6

That reproduces the 6-second prediction from the previous lesson. For the remaining latency examples, freeze a different toy rule, w = [2, 1]. These are chosen coefficients, not the result of the update above; we're studying array operations rather than training:

[2,1]⋅[2,1]=(2×2)+(1×1)=5[2, 1] \cdot [2, 1] = (2 \times 2) + (1 \times 1) = 5[2,1]⋅[2,1]=(2×2)+(1×1)=5

Prompt length contributes four seconds; queue delay contributes one. Two vectors of length two produce one scalar.

Check the difference between *, which keeps the two contributions, and @, which sums them. Both should agree with the hand calculation:

one-request-dot-product.py
1import numpy as np 2 3features = np.array([2.0, 1.0]) 4weights = np.array([2.0, 1.0]) 5contributions = features * weights 6prediction = features @ weights 7 8print("contributions", contributions.tolist()) 9print("prediction_seconds", prediction) 10print("output_shape", prediction.shape) 11assert prediction == sum(a * b for a, b in zip(features, weights, strict=True))
Output
1contributions [4.0, 1.0] 2prediction_seconds 5.0 3output_shape ()

Element-wise multiplication stops at a contribution vector. The dot product adds those contributions into one number. For two real-valued 1-D arrays, NumPy's @ computes this inner product.[1]Reference 1numpy.matmulhttps://numpy.org/doc/stable/reference/generated/numpy.matmul.html

Why can't you swap the feature order to [queue_delay, prompt_blocks] while keeping weights as [seconds per prompt block, seconds per second of queue delay]?

Answer

The lengths would still match, so the code would run, but positions would no longer describe the same quantities. You'd multiply queue delay by the prompt-length weight and prompt length by the queue-delay weight. Length checks catch structural errors; clear axis meaning catches semantic errors.

Now apply that same rule to several requests. We don't need a different weight vector for every row.

Stack requests into a matrix

Put three observed requests into a matrix: a rectangular array whose rows are requests and whose columns are features. The row axis lets one rule run across all requests.

X=[102131]w=[21]X = \begin{bmatrix} 1 & 0 \\ 2 & 1 \\ 3 & 1 \end{bmatrix} \quad w = \begin{bmatrix} 2 \\ 1 \end{bmatrix}X=​123​011​​w=[21​]

Read conceptual shape [3, 2] as three requests by two features. Multiplying every row by the same weight vector returns one prediction per request:

[3,2]  @  [2]→[3][3, 2] \; @ \; [2] \rightarrow [3][3,2]@[2]→[3]

The @ notation is the contract you'll see in library code. Here it means "dot each row with w." A length-two NumPy vector has shape (2,) even though we drew its values vertically in the equation. An explicit column matrix would have shape (2, 1) and would preserve a length-one output axis.

Predict the three results before running this cell: one short prompt with no queue gives 2; the other rows give 5 and 7:

many-requests-one-weight-vector.py
1import numpy as np 2 3X = np.array([ 4 [1.0, 0.0], # short prompt, empty queue 5 [2.0, 1.0], # medium prompt, one second of queue delay 6 [3.0, 1.0], # longer prompt, one second of queue delay 7]) 8weights = np.array([2.0, 1.0]) 9predictions = X @ weights 10 11print("X_shape", X.shape) 12print("weights_shape", weights.shape) 13print("predictions_shape", predictions.shape) 14print("predicted_seconds", predictions.tolist()) 15assert (X @ weights[:, None]).shape == (3, 1)
Output
1X_shape (3, 2) 2weights_shape (2,) 3predictions_shape (3,) 4predicted_seconds [2.0, 5.0, 7.0]

The one-request computation repeats across each row. Feature lengths must agree, and the row axis survives. The final assertion adds a singleton axis with [:, None] to highlight the distinction between a 1D vector and a 2D column matrix.

Those three numbers provide one output per request. But what happens if each request needs two distinct outputs instead of one?

Use a matrix to make several outputs

A weight vector produces one output. A weight matrix produces several. Suppose the latency service also wants an internal escalation-priority score from the same two features. This score uses arbitrary units and handpicked weights; it isn't a probability or a trained decision rule.

Use one column of W for each output:

W=[2.00.21.01.5]W = \begin{bmatrix} 2.0 & 0.2 \\ 1.0 & 1.5 \end{bmatrix}W=[2.01.0​0.21.5​]

The first column computes response seconds; the second computes priority. For x = [2, 1], run both dot products side by side:

xW=[2,1][2.00.21.01.5]=[5.0,1.9]xW = [2, 1] \begin{bmatrix} 2.0 & 0.2 \\ 1.0 & 1.5 \end{bmatrix} = [5.0, 1.9]xW=[2,1][2.01.0​0.21.5​]=[5.0,1.9]

The seconds column is the familiar [2, 1] rule. The priority column is a second dot product against the same features. The shape contract is:

[2]  @  [2,2]→[2][2] \; @ \; [2, 2] \rightarrow [2][2]@[2,2]→[2]

For the whole three-row matrix:

[3,2]  @  [2,2]→[3,2][3, 2] \; @ \; [2, 2] \rightarrow [3, 2][3,2]@[2,2]→[3,2]

This lesson stores examples as rows and outputs as columns, so it writes X @ W. Textbooks that store each example as a column may write W x instead. Both conventions work if axis meanings stay attached to the numbers.

Request matrix X of shape [3, 2] multiplies weight matrix W of shape [2, 2] to produce output matrix Y of shape [3, 2]. Row B features [2.0, 1.0] and weight column seconds [2.0, 1.0] compute entry Y[B, seconds] = 5.0 through a dot product. The inner feature dimension 2 contracts away while request rows and output columns survive.
The highlighted calculation demonstrates the contraction contract: request B's feature row `[2.0, 1.0]` dots with the seconds column `[2.0, 1.0]` to yield `5.0`. The inner feature dimension of size 2 contracts (sums away), leaving 3 request rows and 2 output columns intact.

In the code, W[:, 0] selects the seconds column. Compare its predictions with the corresponding column of the full result:

matrix-transforms-request-features.py
1import numpy as np 2 3X = np.array([[1.0, 0.0], [2.0, 1.0], [3.0, 1.0]]) 4W = np.array([ 5 [2.0, 0.2], # prompt contribution to [seconds, priority] 6 [1.0, 1.5], # queue contribution to [seconds, priority] 7]) 8 9outputs = X @ W 10 11print("contract", X.shape, "@", W.shape, "->", outputs.shape) 12print("second_request", outputs[1].tolist()) 13print("all_seconds_predictions", outputs[:, 0].tolist()) 14np.testing.assert_allclose(outputs[:, 0], X @ W[:, 0])
Output
1contract (3, 2) @ (2, 2) -> (3, 2) 2second_request [5.0, 1.9] 3all_seconds_predictions [2.0, 5.0, 7.0]

Neural-net layers use the same idea at larger width: one learned matrix mixes input features into many output features for every row at once.

The useful debugging question is now concrete: what if a teammate hands you a W whose inner size doesn't match?

Match inner dimensions before multiplying

Suppose W expects three input features:

[3,2]  @  [3,2][3, 2] \; @ \; [3, 2][3,2]@[3,2]

The first object ends with feature count 2; the second starts with 3. There's no third feature to pair, so the multiply has to fail.

Run the invalid multiplication deliberately, then check a compatible matrix. The catch prints our own short diagnosis because NumPy's full error wording can vary by release:

catch-the-matrix-shape-error.py
1import numpy as np 2 3X = np.array([[1.0, 0.0], [2.0, 1.0], [3.0, 1.0]]) 4wrong_W = np.zeros((3, 2)) 5correct_W = np.array([[2.0, 0.2], [1.0, 1.5]]) 6 7try: 8 X @ wrong_W 9except ValueError: 10 print("invalid", X.shape, "@", wrong_W.shape) 11 print("meeting_dimensions", X.shape[1], "and", wrong_W.shape[0], "do_not_match") 12else: 13 raise AssertionError("expected incompatible input-feature dimensions") 14 15print("valid_output_shape", (X @ correct_W).shape)
Output
1invalid (3, 2) @ (3, 2) 2meeting_dimensions 2 and 3 do_not_match 3valid_output_shape (3, 2)

When multiplication fails, don't randomly transpose arrays until the error disappears. Name the axes first: request rows, input features, and output features. Then fix the object whose meaning is wrong.

Requests aren't the only extra axis. A prompt is a sequence of tokens, so a third axis must say which token each feature vector belongs to.

Add token and batch axes with tensors

In library code, a tensor means an array with any number of axes. NumPy calls its array type ndarray; PyTorch calls its type torch.Tensor. The practical array definition is the one used here and in the opening linear algebra chapter of Deep Learning.[2]Reference 2Deep Learning.https://www.deeplearningbook.org/

A scalar has zero axes, a vector one, and a matrix two. The number of axes is often called tensor rank, available as .ndim in NumPy. Don't confuse it with matrix rank, which measures independent directions and comes in the next lesson.

So far, we represented each request with two summary features: prompt length and queue delay. To represent its text, switch to one vector per token. These token coordinates aren't the latency features, even though we'll also give them length two. The NumPy lesson used the same (B, T, D) convention on log snippets:

AxisSymbolMeaning here
batchBseveral prompts processed together
sequenceTtoken positions within each prompt
featureDnumbers describing one token at one layer

Use tiny handcrafted token features so every number stays readable. This isn't a trained embedding table; it's a shape exercise:

TokenFeature 0Feature 1
test-like1.00.0
fixture-like0.50.5
import-like0.01.0

Two prompts, each containing three token-feature vectors, therefore have shape [2, 3, 2].

Tensor H has shape [2, 3, 2] and contains two prompt matrices. Slicing index 1 selects prompt 1 with shape [3, 2], index 2 selects token 2 with shape [2], and index 0 selects feature 0 with scalar value 1.0.
Follow the rank reduction path: tensor [2, 3, 2] (rank 3) strips the batch axis at index 1 to yield matrix [3, 2] (rank 2), strips the sequence axis at index 2 to yield vector [2] (rank 1), and strips the feature axis at index 0 to yield scalar 1.0 (rank 0).

Read that shape as B = 2 prompts, T = 3 token positions, and D = 2 features per token. Indexing is zero-based, so H[1, 2, 0] selects the first feature of the third token in the second prompt. A colon keeps all entries along its axis: H[:, :, 0] therefore keeps batch and sequence but fixes the feature. Run both selections:

stack-prompt-sequences-into-a-tensor.py
1import numpy as np 2 3H = np.array([ 4 [[1.0, 0.0], [0.5, 0.5], [0.0, 1.0]], # "test fixture import" 5 [[0.0, 1.0], [0.5, 0.5], [1.0, 0.0]], # "import fixture test" 6]) 7 8print("tensor_shape", H.shape, "axes", H.ndim) 9print("second_prompt_shape", H[1].shape) 10print("third_token", H[1, 2].tolist(), "shape", H[1, 2].shape) 11print("H[1,2,0]", H[1, 2, 0]) 12print("feature_zero_slice", H[:, :, 0].shape) 13assert H.size == 2 * 3 * 2
Output
1tensor_shape (2, 3, 2) axes 3 2second_prompt_shape (3, 2) 3third_token [1.0, 0.0] shape (2,) 4H[1,2,0] 1.0 5feature_zero_slice (2, 3)

The last axis still contains features. Extra axes only organize which prompt and token each vector belongs to. A learned projection should change that last axis and leave the others alone.

Project each token without mixing positions

In this lesson, a projection is a matrix transformation of each token's features. Neural networks learn its coefficients; we'll choose small ones to inspect the arithmetic. Let matrix P map two input features into three output features:

[B,T,2]  @  [2,3]→[B,T,3][B, T, 2] \; @ \; [2, 3] \rightarrow [B, T, 3][B,T,2]@[2,3]→[B,T,3]

Predict the output before multiplying: batch and sequence should remain intact, while the token-feature size changes from 2 to 3.

project-every-token-vector.py
1import numpy as np 2 3H = np.array([ 4 [[1.0, 0.0], [0.5, 0.5], [0.0, 1.0]], 5 [[0.0, 1.0], [0.5, 0.5], [1.0, 0.0]], 6]) 7P = np.array([ 8 [1.0, 0.0, 0.5], 9 [0.0, 1.0, 0.5], 10]) 11 12projected = H @ P 13by_hand = [[[sum(token[d] * P[d, j] for d in range(2)) 14 for j in range(3)] for token in prompt] for prompt in H] 15 16print("contract", H.shape, "@", P.shape, "->", projected.shape) 17print("first_prompt_first_token", projected[0, 0].tolist()) 18print("middle_token_shared", np.array_equal(projected[0, 1], projected[1, 1])) 19np.testing.assert_allclose(projected, by_hand)
Output
1contract (2, 3, 2) @ (2, 3) -> (2, 3, 3) 2first_prompt_first_token [1.0, 0.0, 0.5] 3middle_token_shared True

Read the inner calculation as "multiply input coordinate d by its coefficient for output coordinate j, then sum over d." The three nested lists repeat that calculation over output features, tokens, and prompts. np.testing.assert_allclose checks the library result against this explicit version, allowing for small floating-point differences.

For a shared projection with shape [D, output_width], each token is transformed independently. Prompts don't mix, and token positions don't mix. More generally, @ multiplies the last two axes and broadcasts compatible leading batch axes.[1]Reference 1numpy.matmulhttps://numpy.org/doc/stable/reference/generated/numpy.matmul.html Here P has no batch axis, so the same matrix serves both prompts.

This also explains an important limit: making the last dimension wider doesn't create new information. Each output is still a weighted combination of the two inputs. A later layer must do more than change width if it needs interactions between token positions.

A token tensor has shape [4, 128, 768] and a projection has shape [768, 256]. What output shape should you expect?

Answer

[4, 128, 256]. The meeting feature dimension 768 contracts, while batch and sequence axes remain intact.

Multiplication usually fails loudly when inner sizes disagree. Addition can stay quiet while still attaching a number to the wrong axis. That makes broadcasting a semantic debugging problem, not only an arithmetic one.

Treat broadcasting as an axis decision

Broadcasting lets a smaller array participate in element-wise operations against a larger one. First choose what should repeat. An output bias [seconds_offset, priority_offset] should repeat across every request row:

[3,2]+[2]→[3,2][3, 2] + [2] \rightarrow [3, 2][3,2]+[2]→[3,2]

NumPy compares dimensions from the right: equal sizes match, a size of 1 can expand, and missing leading dimensions act like size 1. The rule is useful, but it can also hide an axis mistake.[3]Reference 3Broadcasting.https://numpy.org/doc/stable/user/basics.broadcasting.html

Write both additions explicitly, then predict the second request. Feature bias should produce [5.1, 1.8]; row offsets should produce [4.9, 1.8] while still looking valid:

broadcast-the-right-axis.py
1import numpy as np 2 3outputs = np.array([ 4 [2.0, 0.2], 5 [5.0, 1.9], 6 [7.0, 2.1], 7]) # columns: [seconds, priority] 8 9feature_bias = np.array([0.1, -0.1]) # one value per output feature 10row_offsets = np.array([[0.1], [-0.1], [0.0]]) # one value per request 11 12corrected = outputs + feature_bias 13wrong_but_valid = outputs + row_offsets 14 15print("correct_second_request", corrected[1].round(1).tolist()) 16print("wrong_second_request", wrong_but_valid[1].round(1).tolist()) 17assert corrected.shape == wrong_but_valid.shape == (3, 2) 18assert feature_bias.shape == (outputs.shape[-1],) 19assert not np.allclose(corrected, wrong_but_valid)
Output
1correct_second_request [5.1, 1.8] 2wrong_second_request [4.9, 1.8]

Both results have three rows and two columns, but only one uses the intended axis. Checking only the result shape misses this bug. Requiring a one-dimensional bias with length equal to the output width catches the (3, 1) input. You still need named columns and a known-value example to catch two feature biases supplied in the wrong order.

The same dot product that scored one request can compare token vectors with each other. The result changes from one score to a grid of scores.

Read a score grid as repeated dot products

For three token-feature vectors in one prompt, multiply the token matrix by its transpose, which swaps rows and columns. The transpose turns each token row into a column so a row-column dot product compares two tokens. Predict the result shape first:

[T,D]  @  [D,T]→[T,T][T, D] \; @ \; [D, T] \rightarrow [T, T][T,D]@[D,T]→[T,T]

Every entry in the output is one dot product between two token positions. Rows name the first token; columns name the second.

test-likefixture-likeimport-like
test-like1.00.50.0
fixture-like0.50.50.5
import-like0.00.51.0

These are raw alignment scores, not automatically cosine similarities. Notice the middle token's self-score: 0.5² + 0.5² = 0.5, not 1. A dot product depends on vector length as well as direction.

For a real vector vvv, its Euclidean length, written ∥v∥\|v\|∥v∥, is the square root of the sum of its squared coordinates. For nonzero vectors, cosine similarity is a⋅b/(∥a∥∥b∥)a \cdot b / (\|a\|\|b\|)a⋅b/(∥a∥∥b∥).[2]Reference 2Deep Learning.https://www.deeplearningbook.org/ Dividing each vector by its length makes its self-score 1. A zero vector has no direction, so cosine similarity needs an explicit policy for that case.

You can draw a two-coordinate vector as an arrow from (0, 0) to its coordinates. Cosine similarity measures the angle between two such arrows: 1 for the same direction, 0 for perpendicular directions, and -1 for opposite directions. Scaling an arrow by a positive number changes its length but not that angle.

Two geometric plots compare base vector a = [0.5, 0.5] and scaled vector 2a = [1.0, 1.0] against horizontal reference vector u = [1, 0]. Both point at 45 degrees. Vector a has length 0.707, projection 0.5, dot product 0.5, and cosine similarity 0.707. Vector 2a has length 1.414, projection 1.0, dot product 1.0, and identical cosine similarity 0.707.
Both vectors point along the identical 45-degree ray. Doubling vector length doubles the projected shadow and doubles the dot product with [1, 0] from 0.5 to 1.0. Because cosine similarity divides out vector norms, its angular score stays exactly 0.707.

Compute both grids and compare their diagonals. keepdims=True gives lengths shape [3, 1], keeping each length attached to its row during normalization:

token-similarity-grid.py
1import numpy as np 2 3tokens = np.array([ 4 [1.0, 0.0], # test-like 5 [0.5, 0.5], # fixture-like 6 [0.0, 1.0], # import-like 7]) 8 9scores = tokens @ tokens.T # .T swaps the two axes of this matrix 10lengths = np.linalg.norm(tokens, axis=1, keepdims=True) 11if np.any(lengths == 0): 12 raise ValueError("cosine similarity needs nonzero token vectors") 13unit_tokens = tokens / lengths 14cosines = unit_tokens @ unit_tokens.T 15 16print("contract", tokens.shape, "@", tokens.T.shape, "->", scores.shape) 17print("raw_scores", scores.tolist()) 18print("cosine_diagonal", np.diag(cosines).round(6).tolist()) 19np.testing.assert_allclose(np.diag(cosines), 1.0)
Output
1contract (3, 2) @ (2, 3) -> (3, 3) 2raw_scores [[1.0, 0.5, 0.0], [0.5, 0.5, 0.5], [0.0, 0.5, 1.0]] 3cosine_diagonal [1.0, 1.0, 1.0]

Later, attention will create separate query and key vectors, scale the Q @ K.T score grid, and convert each row into weights that sum to one.[4]Reference 4Attention Is All You Need.https://arxiv.org/abs/1706.03762 That expression is for one 2-D pair of matrices. For a batch shaped [B, T, D], use H @ H.swapaxes(-1, -2) to get [B, T, T]. Plain H.T reverses all three axes and would put batch size in the wrong place.

If Q has shape [5, 8] and K has shape [5, 8], why does Q @ K.T have shape [5, 5]?

Answer

Transposing K turns [5, 8] into [8, 5]. The feature dimensions 8 meet, and the surviving dimensions describe five query positions by five key positions.

Build it: trace one tiny prompt projection

Now combine the pieces in a function that accepts a batch of token sequences and a projection matrix. Convert the inputs to numeric arrays first, reject the wrong number of axes, then check feature width. np.asarray(..., dtype=float) also rejects ragged lists such as [[1, 0], [0.5]]; inspecting only the first row wouldn't catch that missing coordinate.

Use the same two-to-three projection as before. After projecting, average over token positions to produce one summary vector per prompt. NumPy's mean(axis=1) removes the sequence axis and retains batch and features.[5]Reference 5numpy.meanhttps://numpy.org/doc/stable/reference/generated/numpy.mean.html

build-it-prompt-projection.py
1import numpy as np 2 3def project_prompts(hidden, projection): 4 try: 5 hidden = np.asarray(hidden, dtype=float) 6 projection = np.asarray(projection, dtype=float) 7 except (TypeError, ValueError) as error: 8 raise ValueError("inputs must be rectangular numeric arrays") from error 9 if hidden.ndim != 3: 10 raise ValueError("expected [batch, sequence, feature]") 11 if projection.ndim != 2: 12 raise ValueError("expected projection [input_feature, output_feature]") 13 if hidden.shape[-1] != projection.shape[0]: 14 raise ValueError("feature axis does not match projection input") 15 if 0 in hidden.shape or 0 in projection.shape: 16 raise ValueError("this exercise expects nonempty axes") 17 return hidden @ projection 18 19hidden = [ 20 [[1.0, 0.0], [0.5, 0.5], [0.0, 1.0]], 21 [[0.0, 1.0], [0.5, 0.5], [1.0, 0.0]], 22] 23projection = [[1.0, 0.0, 0.5], [0.0, 1.0, 0.5]] 24 25projected = project_prompts(hidden, projection) 26prompt_summaries = projected.mean(axis=1) 27 28print("input", np.shape(hidden), "projection", np.shape(projection)) 29print("projected", projected.shape) 30print("prompt_summaries", prompt_summaries.tolist()) 31np.testing.assert_allclose(prompt_summaries, [[0.5, 0.5, 0.5]] * 2) 32 33bad_inputs = [ 34 ("feature", hidden, np.zeros((3, 3))), 35 ("rank", hidden, [0.0, 0.0]), 36 ("ragged", [[[1.0, 0.0], [0.5]]], projection), 37 ("empty", np.empty((2, 0, 2)), projection), 38] 39for name, bad_hidden, bad_projection in bad_inputs: 40 try: 41 project_prompts(bad_hidden, bad_projection) 42 except ValueError as error: 43 print(name, str(error)) 44 else: 45 raise AssertionError(f"{name} input should have failed")
Output
1input (2, 3, 2) projection (2, 3) 2projected (2, 3, 3) 3prompt_summaries [[0.5, 0.5, 0.5], [0.5, 0.5, 0.5]] 4feature feature axis does not match projection input 5rank expected projection [input_feature, output_feature] 6ragged inputs must be rectangular numeric arrays 7empty this exercise expects nonempty axes

The two summaries are identical even though the token order differs. That's expected: a shared token projection followed by a mean is insensitive to permutations of the tokens. Shape checks can't make this summary understand order. Later sequence models introduce position information and interactions between tokens for that reason.

Exclude padding from the mean

Real batches often add padding to make sequences equally long. Even zero-valued padding changes an ordinary mean: two real vectors [1, 0] and [0, 1] average to [0.5, 0.5], but adding two zero rows changes that to [0.25, 0.25].

A mask marks real positions. Add a singleton feature axis so one validity bit applies to every coordinate of its token. Sum only real vectors and divide by the count of real tokens. Reject an all-padding prompt because its mean has no valid denominator:

pool-real-tokens-only.py
1import numpy as np 2 3tokens = np.array([[[1.0, 0.0], [0.0, 1.0], [0.0, 0.0], [0.0, 0.0]]]) 4valid = np.array([[True, True, False, False]]) 5 6def masked_mean(tokens, valid): 7 if tokens.ndim != 3 or valid.shape != tokens.shape[:2]: 8 raise ValueError("mask must match [batch, sequence]") 9 if valid.dtype != np.bool_: 10 raise ValueError("mask must be boolean") 11 counts = valid.sum(axis=1, keepdims=True) 12 if np.any(counts == 0): 13 raise ValueError("each prompt needs at least one real token") 14 selected = np.where(valid[:, :, None], tokens, 0.0) 15 return selected.sum(axis=1) / counts 16 17summary = masked_mean(tokens, valid) 18print("plain_mean", tokens.mean(axis=1).tolist()) 19print("masked_mean", summary.tolist()) 20np.testing.assert_allclose(summary, tokens[:, :2].mean(axis=1)) 21try: 22 masked_mean(tokens, np.zeros_like(valid)) 23except ValueError as error: 24 print("empty_prompt", str(error)) 25else: 26 raise AssertionError("an all-padding prompt must fail")
Output
1plain_mean [[0.25, 0.25]] 2masked_mean [[0.5, 0.5]] 3empty_prompt each prompt needs at least one real token

The mask fixes padding bias, not lost word order. Those are separate properties to test. This example assumes finite real-token values; a mask isn't a substitute for validating the numeric data you keep.

Real token vectors won't be handwritten, and learned projections will be much wider. The invariant stays small enough to say aloud: preserve batch and sequence; match and transform feature size.

Recognize the contract inside a neural-network layer

You've now executed four related operations. In this table, N counts rows, D is input width, and P is output width; B and T still mean batch and sequence. Read each contract by identifying the feature axis that's summed away:

Left @ rightWhat contractsWhat survives
[D] @ [D]matching lengtha scalar
[N, D] @ [D]feature length[N]
[N, D] @ [D, P]D[N, P]
[B, T, D] @ [D, P]D[B, T, P]

One orientation detail matters when you read model code. PyTorch's torch.nn.Linear(in_features, out_features) stores weight as [out_features, in_features], the transpose of our W. It computes input @ weight.T + bias and preserves all input axes except the last, which becomes out_features.[6]Reference 6Linearhttps://docs.pytorch.org/docs/stable/generated/torch.nn.Linear.html So Linear(2, 3) can receive [B, T, 2] directly and return [B, T, 3]. You don't need to flatten the batch or tokens first.

That's an array convention, not a different kind of algebra. Before transposing anything, check how the API names its input and output axes.

Test shapes before code

Keep the convention fixed: rows represent requests or token positions, and the final dimension represents features. Predict each result before checking the sketch:

  1. A latency feature vector has shape [3], and a weight vector has shape [3]. What does x @ w return?
  2. A training table X has shape [20, 3], and W has shape [3, 4]. What does X @ W mean and what shape does it produce?
  3. Prompt hidden states have shape [8, 12, 64], and a projection has shape [64, 16]. Which axes remain unchanged?
  4. Scores have shape [20, 4]. Why can adding a value of shape [20, 1] be a silent bug when you meant a four-feature bias?
  5. Token vectors have shape [12, 64]. What transpose is needed to create a pairwise score grid?
  6. For H with shape [2, 3, 2], what scalar does H[1, 2, 0] select, and what shape does a slice that keeps every prompt and token but fixes feature 0 have?

Solution sketches

  1. It returns a scalar because matching feature positions are multiplied and summed.
  2. It applies one four-output transformation to each of 20 rows, producing [20, 4].
  3. Batch size 8 and sequence length 12 stay unchanged; feature size changes from 64 to 16, giving [8, 12, 16].
  4. [20, 1] legally broadcasts one different value across all four output columns in each row. The output shape looks right even though the adjustment is attached to requests instead of features.
  5. Use the transpose [64, 12]; then [12, 64] @ [64, 12] produces [12, 12].
  6. H[1, 2, 0] selects the first feature of the third token in the second prompt, which is 1.0 in the running tensor. The feature-0 slice keeps prompt and token axes, giving shape [2, 3].

Carry shape contracts forward

Every model operation now reads as a contract:

ObjectExample shapeQuestion to ask
Vector[2]What does each feature position mean?
Matrix of rows[3, 2]What does one row describe?
Weight matrix[2, 3]Which input feature axis must match?
Tensor[2, 3, 2]What do batch, sequence, and feature axes mean?
IndexingH[b, t, d]Which prompt, token position, and feature does this value select?
Similarity grid[3, 3]Which vector pairs produced these dot products?

Before running a multiply, predict its output. When it fails, name the meeting axes. When it broadcasts legally, ask whether the repeated value still means what you intended.

You can now name a latency row, stack rows into a matrix, and project a token tensor without guessing shapes. The next question is whether the matrix carries genuinely different feature directions or repeats the same mixture. Matrix rank counts independent directions; singular value decomposition (SVD) reveals their strengths. Those are the next lesson's questions.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A latency vector is documented as [prompt_blocks, queue_delay], with weights in the same semantic order. A pipeline sends [queue_delay, prompt_blocks] but keeps both lengths equal. What failure can occur?

Correct answer: The operation runs but pairs weights with the wrong features; a known-value test can catch the changed prediction.

Array shapes describe arrangement, not meaning. A length-two dot product always pairs position zero with position zero, so a permutation silently changes the model's feature interpretation.

2.A request uses features x=[3,2] and weights w=[1.5,4.0] in matching order. Which operation returns one latency prediction?

Correct answer: Sum corresponding products: 3*1.5 + 2*4 = 12.5.

Elementwise multiplication gives [4.5, 8]; the dot product adds those contributions. Averaging them introduces an extra factor of one half that isn't part of this prediction rule.

3.A request table X has shape [20,3], and a weight matrix W has shape [3,4] with one column per output feature. What is the shape and meaning of X @ W?

Correct answer: Shape [20,4]: four output values for each of the 20 requests.

Matrix multiplication contracts X's last axis of length three with W's first axis of length three. The surviving row axis and W's output-column axis form [20,4].

4.A projection expects hidden tensors [batch,sequence,feature] and matrices [input_feature,output_feature]. For hidden [4,10,32] and projection [16,64], which guard should reject the call?

Correct answer: Reject the feature width: each token supplies 32 features but the projection expects 16.

The last hidden axis is the vector consumed independently at each batch and token position. Transposing can change orientation but cannot invent the missing agreement between feature meanings.

5.A hidden tensor has shape [2,3,2] and a projection has shape [2,3]. Under [B,T,D] @ [D,P], what result shape preserves independent prompt and token positions?

Correct answer: [2,3,3]: two prompts, three tokens each, three output features per token.

NumPy and PyTorch treat the leading dimensions as a stack of matrices for this case. Only the meeting feature axis contracts; each of the six token vectors receives the same 2-to-3 projection.

6.An output array has shape [3,2] with columns [seconds,priority]. You intend a separately configurable bias for each column. Which shape expresses that meaning?

Correct answer: [2] or [1,2]: one value per output column, repeated across requests.

Broadcasting aligns trailing axes. The final dimension of length two represents output features, so a length-two bias is semantic; a [3,1] array is legal but row-specific.

7.Token vectors have shape [4,6], with four positions and six features. Which product forms raw pairwise position scores?

Correct answer: tokens @ tokens.T, giving a [4,4] matrix of position-pair dot products summed over features.

The transpose changes [4,6] to [6,4], so the inner six dimensions meet and the result indexes four query and four key positions. Raw dot products are not cosine similarities without explicit normalization.

8.A prompt contains real vectors [1,0] and [0,1], followed by two zero-padding vectors. Which mean summary correctly excludes padding?

Correct answer: [0.5,0.5]: sum the real vectors and divide by their valid count two.

A plain reduction has no notion of token validity. Masked pooling excludes padding and divides by the number of real positions, preserving summary meaning across different padded lengths.

9.For H shaped [2,3,2] with axes [batch,sequence,feature], what does H[1,2,0] select, and what shape has H[:,:,0]?

Correct answer: A scalar at prompt 1, token 2, feature 0; H[:,:,0] has shape [2,3].

Indices consume axes in order and are zero based. Each integer removes an axis; each full slice retains it. To keep a singleton feature axis, use H[:,:,0:1]. The shape alone doesn't tell you the selected scalar's value.

9 questions remaining.

Next Step
Continue to Linear Algebra for ML

You can now trace a named vector, a row matrix, a token tensor, and a dot-product grid without guessing shapes. Next you'll look inside a matrix of rows and measure which feature mixes are actually strong, using SVD, rank, and conditioning.

PreviousGradients and Backprop
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

numpy.matmul

NumPy Developers · 2026

https://numpy.org/doc/stable/reference/generated/numpy.matmul.html

Deep Learning.

Goodfellow, I., Bengio, Y., Courville, A. · 2016

https://www.deeplearningbook.org/

Broadcasting.

NumPy Contributors · 2026 · Official NumPy documentation

https://numpy.org/doc/stable/user/basics.broadcasting.html

Attention Is All You Need.

Vaswani, A., et al. · 2017

https://arxiv.org/abs/1706.03762

numpy.mean

NumPy Developers · 2026

https://numpy.org/doc/stable/reference/generated/numpy.mean.html

Linear

PyTorch Contributors · 2026

https://docs.pytorch.org/docs/stable/generated/torch.nn.Linear.html

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.