How to Fix 'RuntimeError: mat1 and mat2 shapes cannot be multiplied' in PyTorch on Linux
PyTorch & CUDA

How to Fix 'RuntimeError: mat1 and mat2 shapes cannot be multiplied' in PyTorch on Linux

Aligning Linear Layer Dimensions and Flattened Spatial Tensors

RuntimeError: mat1 and mat2 shapes cannot be multiplied (A x B and C x D) occurs during forward passes when the inner dimensions of two multiplied matrices do not match (B != C). In standard neural network layers like torch.nn.Linear(in_features, out_features), the layer’s internal weight matrix is transposed to shape (out_features, in_features). Therefore, the last dimension of your incoming input tensor must exactly match in_features.

To resolve the error immediately:

  1. Inspect the incoming tensor shape right before the failing linear or projection layer by printing x.shape.
  2. Update the layer’s in_features argument to match the printed dimension:
# Before the crash: print the exact incoming shape
print("Incoming shape to linear layer:", x.shape)  # e.g., torch.Size([64, 512])

# Update layer definition to match the last dimension:
# If incoming is [64, 512] and layer was nn.Linear(256, 128) -> B (512) != C (256)
self.fc = torch.nn.Linear(512, 128)  # Fixed: in_features = 512

Alternatively, use torch.nn.LazyLinear(out_features) in PyTorch 2.0+. It dynamically inspects the tensor dimension during the first forward pass and initializes in_features automatically without manual math.

Matrix 1 (mat1) Matrix 2 (mat2) Valid Condition Result Shape
Input Tensor: (Batch, B) Linear Weight: (B, Out) B == B (Valid) (Batch, Out)
Example: (64, 512) Linear Weight: (512, 128) 512 == 512 (64, 128)
Example: (64, 128) Linear Weight: (256, 512) 128 != 256 FAIL CRASH

1. Mathematical Root Cause of the Error

In linear algebra and torch.matmul, multiplying two matrices requires the column count of the first matrix to equal the row count of the second matrix:

(M x K) @ (K x N) = (M x N)

In PyTorch, torch.nn.Linear(in_features, out_features) computes:

y = x @ W.T + b

Because W is stored internally with shape (out_features, in_features), its transpose W.T has shape (in_features, out_features). When PyTorch calls the BLAS kernel, mat1 is your incoming tensor x with shape (Batch, in_dim), and mat2 is W.T with shape (in_features, out_features).

+-----------------------------------------------------------------------------------------+
|                  MATRIX MULTIPLICATION INNER DIMENSION ALIGNMENT                        |
+-----------------------------------------------------------------------------------------+
|                                                                                         |
|       mat1 (Input Tensor x)              mat2 (Layer Weight W.T)                        |
|       +-------------------+               +-------------------+                         |
|       |                   |               |                   |                         |
|  Row  |     Batch: 64     |          Row  |  in_features: 512 |                         |
|  (A)  |                   |          (C)  |                   |                         |
|       +-------------------+               +-------------------+                         |
|       |  Columns (B): 512 |               | Columns (D): 128  |                         |
|       +-------------------+               +-------------------+                         |
|                 |                                   |                                   |
|                 +======== MUST BE EQUAL ============+                                   |
|                          (B == C: 512 == 512)                                           |
|                                                                                         |
|  If B != C (e.g., 128 != 256), the C++ BLAS kernel throws:                              |
|  "RuntimeError: mat1 and mat2 shapes cannot be multiplied (64x128 and 256x512)"         |
+-----------------------------------------------------------------------------------------+

When building custom model heads or training agents like our 50-line autonomous Python agent, unhandled tensor dimension mismatches abort training steps immediately. It fails. The error trace points to BLAS. Checking dimensions dynamically saves hours of silent debugging.


2. The 4 Most Common Scenarios & Their Fixes

Scenario 1: Flattening Feature Maps from CNNs or Vision Backbones

The most frequent occurrence happens when transitioning from spatial layers (Conv2d, pooling) to a fully connected classification head (nn.Linear).

Consider this broken snippet:

import torch
import torch.nn as nn

class BrokenConvNet(nn.Module):
    def __init__(self):
        super().__init__()
        # Input image: (batch, 3, 32, 32)
        self.conv = nn.Conv2d(3, 16, kernel_size=3, padding=1)  # output: (batch, 16, 32, 32)
        self.pool = nn.MaxPool2d(2, 2)                          # output: (batch, 16, 16, 16)
        # GUESSING the flattened dimension:
        self.fc = nn.Linear(1024, 10)  # Incorrect! 16 * 16 * 16 = 4096, not 1024

    def forward(self, x):
        x = self.pool(self.conv(x))
        x = x.flatten(1)  # shape is (batch, 4096)
        x = self.fc(x)    # CRASH: mat1 is (batch, 4096), mat2 is (1024, 10)
        return x

model = BrokenConvNet()
inputs = torch.randn(32, 3, 32, 32)
# model(inputs) -> RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x4096 and 1024x10)

The Fix: Calculate the Flattened Dimension or Use Adaptive Pooling

You have two clean options.

Option A: Calculate the spatial volume explicitly: Channels * Height * Width = 16 * 16 * 16 = 4096. Update nn.Linear(4096, 10).

Option B: Add an nn.AdaptiveAvgPool2d((1, 1)) layer before the linear classifier. This pools spatial dimensions to 1x1 regardless of input image resolution, guaranteeing that in_features equals the channel count:

class FixedConvNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(3, 16, kernel_size=3, padding=1)
        self.adaptive_pool = nn.AdaptiveAvgPool2d((1, 1))  # Output: (batch, 16, 1, 1)
        self.fc = nn.Linear(16, 10)  # in_features is always 16

    def forward(self, x):
        x = self.conv(x)
        x = self.adaptive_pool(x)
        x = x.flatten(1)  # shape: (batch, 16)
        return self.fc(x)  # Multiplies (batch, 16) by (16, 10) -> (batch, 10)

Scenario 2: Dynamic Batch Sizes & Trailing Remainder Batches

When reshaping tensors with hardcoded batch sizes (x.view(32, 512)), the last batch produced by a PyTorch DataLoader frequently has fewer items than the configured batch_size (unless drop_last=True).

If your training set has 1,000 items and batch_size=64, the final batch contains only 40 items (1000 % 64 = 40).

# Broken reshaping:
def forward(self, x):
    # Hardcoding batch size 64:
    x = x.view(64, -1)  # CRASH on final batch of size 40!
    return self.fc(x)

The Fix: Preserve Dynamic Batch Dimension with -1 or .flatten(1)

Always preserve the leading dimension dynamically:

# Correct: Preserve batch dimension
def forward(self, x):
    x = x.view(x.size(0), -1)  # or x.flatten(start_dim=1)
    return self.fc(x)

If memory layout errors occur during tensor reshaping, review our detailed guide on how to fix view size is not compatible with tensor size and stride in PyTorch.


Scenario 3: Custom Transformer Attention Projections (QKV Splitting)

In custom self-attention mechanisms or LoRA adapters, multi-head projection layers must split hidden dimensions into (num_heads, head_dim).

If your embedding dimension is 768 and you configure 16 attention heads, each head requires 768 / 16 = 48 dimensions. If an engineer specifies head_dim = 64, the projected tensor becomes 16 * 64 = 1024, causing immediate matrix multiplication failures against downstream linear layers:

import torch
import torch.nn as nn

class BrokenAttentionProjection(nn.Module):
    def __init__(self, embed_dim=768, num_heads=16, head_dim=64):
        super().__init__()
        # 16 * 64 = 1024 != 768!
        self.q_proj = nn.Linear(embed_dim, num_heads * head_dim)  # (768, 1024)
        self.out_proj = nn.Linear(embed_dim, embed_dim)          # Expects (768, 768)

    def forward(self, x):
        # x shape: (batch, seq_len, 768)
        q = self.q_proj(x)       # shape: (batch, seq_len, 1024)
        # Attempting out_proj directly on 1024 features:
        return self.out_proj(q)  # CRASH: (batch*seq, 1024) @ (768, 768)

The Fix: Enforce Strict Mathematical Divisibility

Verify that embed_dim == num_heads * head_dim:

class FixedAttentionProjection(nn.Module):
    def __init__(self, embed_dim=768, num_heads=12):
        super().__init__()
        assert embed_dim % num_heads == 0, "embed_dim must be divisible by num_heads"
        self.head_dim = embed_dim // num_heads  # 768 // 12 = 64
        self.q_proj = nn.Linear(embed_dim, embed_dim)
        self.out_proj = nn.Linear(embed_dim, embed_dim)

    def forward(self, x):
        q = self.q_proj(x)
        return self.out_proj(q)

If multiple devices are used during distributed training, ensure projection layers reside on the proper rank to prevent device mismatch errors covered in our guide on resolving tensors on different devices in PyTorch.


Scenario 4: The Modern Solution: torch.nn.LazyLinear

In modern PyTorch (version 2.0 and later), you can bypass manual dimension calculation entirely using LazyLinear.

A lazy layer defers parameter initialization until the first forward pass, when it inspects the exact shape of incoming tensors:

import torch
import torch.nn as nn

class ModernVisionClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3, 32, kernel_size=3),
            nn.ReLU(),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 64, kernel_size=3),
            nn.ReLU(),
            nn.AdaptiveAvgPool2d((4, 4))
        )
        # No need to manually multiply 64 * 4 * 4!
        # LazyLinear infers in_features automatically during first forward pass
        self.classifier = nn.LazyLinear(out_features=10)

    def forward(self, x):
        x = self.features(x)
        x = x.flatten(1)
        return self.classifier(x)

# Initialize model and run sample batch
model = ModernVisionClassifier()
dummy_input = torch.randn(16, 3, 64, 64)
output = model(dummy_input)
print("Inferred in_features:", model.classifier.in_features)  # Inferred: 1024
print("Output shape:", output.shape)                          # torch.Size([16, 10])

The layer dynamically configured in_features = 1024. No manual math. No shape crash. Clean execution.


3. Production Verification Script

Run this diagnostic test script on Linux to verify that your model architecture handles dynamic batches, flat projections, and multi-head tensors without dimension crashes:

import torch
import torch.nn as nn

def test_model_dimensions():
    print("=" * 60)
    print("PYTORCH TENSOR DIMENSION CONTRACT SUITE")
    print("=" * 60)
    
    # 1. Define sample multi-layer perceptron with projection head
    in_features = 256
    hidden_dim = 512
    out_features = 10
    
    layer1 = nn.Linear(in_features, hidden_dim)
    layer2 = nn.Linear(hidden_dim, out_features)
    
    # 2. Test standard batch
    batch_standard = torch.randn(32, in_features)
    out1 = layer1(batch_standard)
    out2 = layer2(out1)
    print(f"[PASS] Standard Batch (32): Input {list(batch_standard.shape)} -> Output {list(out2.shape)}")
    
    # 3. Test dynamic remainder batch (batch size = 7)
    batch_remainder = torch.randn(7, in_features)
    out_rem = layer2(layer1(batch_remainder))
    print(f"[PASS] Remainder Batch (7) : Input {list(batch_remainder.shape)} -> Output {list(out_rem.shape)}")
    
    # 4. Intentionally induce dimension mismatch to demonstrate diagnostic catch
    mismatched_tensor = torch.randn(32, 128)  # 128 != in_features (256)
    try:
        layer1(mismatched_tensor)
    except RuntimeError as e:
        print("\n[VERIFIED DIAGNOSTIC] Caught expected shape failure:")
        print(f" -> {e}")
        print("Fix: Adjust mismatched_tensor last dimension from 128 to 256.")

    print("=" * 60)
    print("STATUS: ALL DIMENSION CONTRACTS VERIFIED")
    print("=" * 60)

if __name__ == "__main__":
    test_model_dimensions()

Run the script in your terminal:

python3 test_dimensions.py

Expected output:

============================================================
PYTORCH TENSOR DIMENSION CONTRACT SUITE
============================================================
[PASS] Standard Batch (32): Input [32, 256] -> Output [32, 10]
[PASS] Remainder Batch (7) : Input [7, 256] -> Output [7, 10]

[VERIFIED DIAGNOSTIC] Caught expected shape failure:
 -> mat1 and mat2 shapes cannot be multiplied (32x128 and 256x512)
Fix: Adjust mismatched_tensor last dimension from 128 to 256.
============================================================
STATUS: ALL DIMENSION CONTRACTS VERIFIED
============================================================

If matrix multiplication executes on GPU devices but fails with allocator errors, review our troubleshooting guide on resolving CUDA error CUBLAS status alloc failed in PyTorch.

If your environment cannot detect NVIDIA GPUs during model initialization, check our walkthrough on fixing torch.cuda.is_available() returning False on Linux.


4. Summary Checklist for Dimension Alignment

  1. Verify Inner Dimensions: Ensure that the last dimension of your input tensor equals in_features of nn.Linear.
  2. Flatten Explicitly: Always pass start_dim=1 to x.flatten(1) to protect the leading batch dimension.
  3. Use Adaptive Pooling: Insert nn.AdaptiveAvgPool2d((1, 1)) between convolutional backbones and linear classification heads.
  4. Adopt Lazy Layers: Replace static nn.Linear layers with nn.LazyLinear when input spatial resolutions vary across datasets.
  5. Print Tensor Shapes: Insert print(x.shape) immediately preceding any failing layer before modifying model architectures.
BC

Written by B. Cho

Lead AI Systems & Linux Infrastructure Engineer

Specializing in GPU inference engines (vLLM, SGLang, Ollama), CUDA memory allocation, and Model Context Protocol (MCP) infrastructure on Linux workstations.