teaching_llm_applications

Resource accounting

    import torch
    x = torch.zeros(4, 8)  
    assert x.dtype == torch.float32  # Default type
    assert x.numel() == 4 * 8
    assert x.element_size() == 4  # Float is 4 bytes
    assert get_memory_usage(x) == 4 * 8 * 4  # 128 bytes
    # One matrix in the feedforward layer of GPT-3:
    assert get_memory_usage(torch.empty(12288 * 4, 12288)) == 2304 * 1024 * 1024  # 2.3 GB 

x = torch.zeros(4, 8, dtype=torch.float16)  
    assert x.element_size() == 2
    However, the dynamic range (especially for small numbers) isn't great.
    x = torch.tensor([1e-8], dtype=torch.float16)  
    assert x == 0  # Underflow!
    
def einops_motivation():
    #Easy to mess up the dimensions (what is -2, -1?)...
    Traditional PyTorch code:
    x = torch.ones(2, 2, 3)      # batch seq hidden  
    y = torch.ones(2, 2, 3)      # batch seq hidden  
    z = x @ y.transpose(-2, -1)  # batch seq seq  
    #Easy to mess up the dimensions (what is -2, -1?)...


def einops_einsum():
    #Einsum is generalized matrix multiplication with good bookkeeping.
    x = torch.ones(3, 4)  # seq1 hidden 
    y = torch.ones(4, 3)  # hidden seq2 
    # Old way
    z = x @ y   # seq1 seq2  
    # New (einops) way
    z = einsum(x, y, "seq1 hidden, hidden seq2 -> seq1 seq2")    

    # Let's try a more complex example...
    x = torch.ones(2, 3, 4)  # batch seq1 hidden 
    y = torch.ones(2, 3, 4)  # batch seq2 hidden 
    # Old way
    z = x @ y.transpose(-2, -1)  # batch seq1 seq2  
    # New (einops) way
    z = einsum(x, y, "batch seq1 hidden, batch seq2 hidden -> batch seq1 seq2")  
    # Dimensions that are not named in the output are summed over.  
    total_flops = 6 * 70e9 * 15e12  
    h100_flop_per_sec = 1979e12 / 2
    mfu = 0.5
    flops_per_day = h100_flop_per_sec * mfu * 1024 * 60 * 60 * 24  
    days = total_flops / flops_per_day  
def arithmetic_intensity_gelu():
    n = 1024
    x = torch.ones(n, dtype=torch.bfloat16, device=cuda_if_available())
    y = F.gelu(x)  # GELU(x) = 0.5 x (1 + tanh(sqrt(2/pi) (x + 0.044715 x^3)))
    bytes = (2 * n) + (2 * n)  # Read x, write y (bf16 is 2 bytes/float)
    flops = 20 * n  # tanh can approximated in various ways (e.g., polynomial)
    arithmetic_intensity = flops / bytes  
    h100_accelerator_intensity = h100_flop_per_sec / h100_bytes_per_sec  
    assert arithmetic_intensity < h100_accelerator_intensity

    # Note that GeLU does more work than ReLU per byte moved, so it has higher arithmetic intensity.
    # But still memory-bound!
    # In other words, ReLU is not faster than GeLU (when doing things in an isolated way).
def arithmetic_intensity_dot_product():
    n = 1024
    x = torch.ones(n, dtype=torch.bfloat16, device=cuda_if_available())
    w = torch.ones(n, dtype=torch.bfloat16, device=cuda_if_available())
    y = x @ w
    bytes = (2 * n) + (2 * n) + 2  # Read x, read w, write y
    flops = 2 * n - 1  # n multiplications, n-1 additions
    arithmetic_intensity = flops / bytes  # ~1/2 
    h100_accelerator_intensity = h100_flop_per_sec / h100_bytes_per_sec  
    assert arithmetic_intensity < h100_accelerator_intensity
    # Memory-bound!

Deep networks cost

def deep_linear_network():
    
    Consider a deep network with L layers and D-dimensional inputs, activations, and outputs.
    # Define the network
    D = 8  # Dimensionality of input, activations, and output
    L = 3  # Number of layers
    model = DeepNetwork(dim=D, num_layers=L).to(cuda_if_available())
    num_parameters = get_num_parameters(model)  
    assert num_parameters == (D * D) * L
    # Run the model on a batch of data
    B = 4  # Batch size
    x = torch.randn(B, D, device=cuda_if_available())  
    y = model(x)  

class Block(nn.Module):
    """Simple block that applies a linear transformation followed by a ReLU nonlinearity."""
    def __init__(self, dim: int):
        super().__init__()
        self.weight = nn.Parameter(torch.randn(dim, dim) / np.sqrt(dim))
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        x = x @ self.weight  # Linear
        x = F.relu(x)        # Activation
        return x

class DeepNetwork(nn.Module):
    """Map `dim`-vector to a `dim`-vector."""
    def __init__(self, dim: int, num_layers: int):
        super().__init__()
        self.layers = nn.ModuleList([Block(dim) for i in range(num_layers)])
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        # Apply all the layers sequentially
        for layer in self.layers:
            x = layer(x)  
        return x

Summary