Blog AI Frontiers

  • The Reality of Scaling: How I Stress-Tested My Dual-GPU Rig Against OpenAI’s Laws

    Future of Work with AI Agents: LLM Scaling Laws, compute-optimal training
    Future of Work with AI Agents: LLM Scaling Laws, compute-optimal training

    After publishing my overview of the LLM Scaling Laws, I was left with a nagging question: Does this actually hold up when you aren’t training on a massive cluster? Theoretical comprehension is one thing, but as I’ve discussed in my previous posts, Implementation-First Research requires getting your hands dirty.

    So, I decided to take my local Ubuntu workstation — the dual RTX 4080 “beast” — and run a series of controlled experiments to reproduce the power-law curves for N (parameters) and C (compute).

    Here is the “DIY report” of what it takes to turn 8×109 FLOPs of theory into actual training runs.


    The Experiment Design: Sharding the Laws

    The goal was to verify the relationship L(N)∝N−0.07. I needed to train five different model architectures, ranging from 5 million to 120 million parameters, keeping the dataset (a cleaned subset of OpenWebText) constant.

    The “Do-It-Yourself” Setup:

    • Engine: PyTorch + HuggingFace Accelerate.
    • Parallelism: Data Parallelism across both RTX 4080s.
    • The Goal: Plot the cross-entropy loss against the total compute (FLOPs) used during training.

    Technical Execution: Making the Code Efficient

    LLM Scaling Laws: to make this reproducible for anyone with a decent GPU, I had to solve the “Batch Size Problem.” Scaling laws depend on a specific “critical batch size” (Bcrit​). If you exceed it, you waste compute; if you stay below it, your GPUs underutilize.

    Here is the code I used to calculate the approximate FLOPs for my runs, which is essential if you want to see if you’re actually following the “laws”:

    Python

    def calculate_training_flops(params, num_tokens):
        """
        Standard approximation for Transformer training compute.
        C ≈ 6 * N * D 
        """
        return 6 * params * num_tokens
    
    # My monitoring setup for dual GPUs
    from accelerate import Accelerator
    
    accelerator = Accelerator(mixed_precision="bf16") # Essential for 40-series cards
    device = accelerator.device
    
    def train_iteration(model, batch, optimizer):
        with accelerator.autocast():
            outputs = model(batch['input_ids'], labels=batch['labels'])
            loss = outputs.loss
        
        accelerator.backward(loss)
        optimizer.step()
        optimizer.zero_grad()
        return loss
    

    The “Bare Metal” Hurdles: What the Papers Don’t Tell You

    1. Thermal Throttling is Your Enemy: During the 120M parameter run, my secondary GPU hit 84°C. On Ubuntu, I had to use nvidia-settings to manually override the fan curve to 90% speed. Local AI research sounds quiet until you’re 4 hours into a training run and your office sounds like a jet engine.
    2. The VRAM Bottleneck: Even with 32GB of combined VRAM, I realized that for larger models, the optimizer states (AdamW) take up more room than the model itself.
      • Pro-tip: Switch to AdamW8bit from the bitsandbytes library. It cut my memory footprint by almost 35% with zero noticeable impact on the scaling curve accuracy.

    Implementation Tip: Handling Data Loaders

    If you’re reproducing this on a local machine, your SSD might become the bottleneck. I had to move from standard JSON loading to pre-tokenized .bin files to keep my GPUs at 100% utilization.

    Python

    import numpy as np
    
    class PreTokenizedDataset(torch.utils.data.Dataset):
        def __init__(self, file_path, block_size):
            # Memory-mapping the data so we don't load 50GB into RAM
            self.data = np.memmap(file_path, dtype=np.uint16, mode='r')
            self.block_size = block_size
    
        def __getitem__(self, i):
            x = torch.from_numpy((self.data[i:i+self.block_size]).astype(np.int64))
            y = torch.from_numpy((self.data[i+1:i+1+self.block_size]).astype(np.int64))
            return x, y
    

    The Results: Does the Math Hold Up Locally?

    After 48 hours of constant compute, I plotted my results on a log-log scale.

    The Verdict: the approach LLM Scaling Laws is beautiful. My empirical curve for the 5M to 120M models followed the predicted slope with an R-squared of 0.98. This proves that Scaling Laws are fractal — they work just as predictably at the “DIY scale” as they do at the “OpenAI scale.”

    Total Resources Used:

    • Total Compute: Approx. 1.2×1018 FLOPs.
    • Electricity: Around 35 kWh.
    • VRAM Peak: 14.2 GB per card (on the 120M model).

    Value for the Reader: Why Should You Do This?

    Most people treat Scaling Laws as a “given,” something they read about in a blog post and move on. But reproducing them on your own hardware gives you “Compute Intuition.” When you see exactly how the loss stalls when you don’t have enough data (D), or how the loss drops when you increase parameters (N), you stop guessing. You start engineering.

    If you want to replicate this, my advice is:

    1. Start Small: Don’t try to train a 7B model. Start with 10M. The math is the same.
    2. Monitor Everything: Use Weights & Biases or TensorBoard. If you don’t see a straight line on a log-log plot, something is wrong with your data loader or your learning rate schedule.
    3. Optimize for Ubuntu: Native CUDA drivers are non-negotiable for stability during 48-hour runs.

    Final Thoughts

    Reproducing “Scaling Laws for Language Model Training” wasn’t just a test of my GPUs — it was a test of my understanding of the fundamental physics of AI. We are living in an era where an individual with $3,000 worth of hardware can verify the laws that govern the world’s most powerful models.

    See also: https://arxiv.org/abs/2001.08361

    While scaling laws predict lower loss with more compute, they don’t necessarily guarantee genuine logical reasoning, a topic I explored in my analysis of the strengths and limitations of reasoning models.

  • Beyond the Hype: Why I Built a Local Dual-GPU Rig for Implementation-First AI Research

    Graphics (The Heart): Dual Nvidia RTX 4080 GPUs for AI research

    Graphics (The Heart): Dual Nvidia RTX 4080 GPUs

    Let’s cut through the hype: most AI research assumes you have a massive budget, but in my homelab, reality is measured in GPU temps and Python execution speed. I’m stripping away the fluff to see which ‘frontiers’ actually matter when you’re running on bare metal. Let’s see what’s worth our compute cycles.

    If you’ve spent any time reading my thoughts over at AI Frontiers, you know I have a bit of an obsession with what’s “under the hood.” For me, AI isn’t about flashy prompts or talking to a black box; it’s about the linear algebra, the stochastic processes, and the raw optimization theory that makes the magic happen.

    But here’s the thing: you can’t truly dismantle the “black box” of AI if you’re always renting someone else’s brain.

    For the longest time, I relied on cloud instances. It’s the standard advice, right? “Don’t buy hardware, just use AWS or Lambda Labs.” But as an independent researcher, I found that the cloud creates a psychological barrier to experimentation. When the “meter is running” at $2.00 an hour, you don’t take risks. You don’t try that weird, non-standard optimization path. You play it safe.

    To truly bridge the gap between abstract theory and functional code—a core mission of mine—I needed a local powerhouse. So, I built one. Here is the “why” and the “how” behind my dual-RTX 4080 workstation.


    The Philosophy of the AI Build: VRAM is the Currency of Research

    In AI research, VRAM (Video RAM) isn’t just a spec; it’s your ceiling. If you don’t have enough, you simply can’t load the model. While the RTX 4090 is the darling of the internet, the current market is… let’s just say, volatile.

    I opted for Dual Nvidia RTX 4080s. This gives me a combined 32GB of high-speed GDDR6X VRAM.

    Why two cards instead of one? Because it forces me to deal with Model Sharding and Parallelism. If I want to reconstruct a groundbreaking paper from scratch and the model is too big for a consumer card, I have to understand how to “slice” that model. This is where the math meets the metal.

    The Architecture: Ubuntu and the Bare Metal

    I run this on Ubuntu Linux. No WSL2, no Windows overhead. When you are tracking the pulse of AI from conferences like ICLR or NeurIPS, you’re looking at code that is native to Linux. Ubuntu gives me direct access to the CUDA drivers and the NCCL (NVIDIA Collective Communications Library) backend, which is essential for making two GPUs talk to each other without a massive latency penalty.


    Getting Technical: Sharding and Distributed Inference

    One of my goals is Implementation-First Research. When a new LLM architecture drops, I want to see the loss curves on my own hardware. To do that with 32GB of VRAM across two cards, I use DeepSpeed or PyTorch’s Fully Sharded Data Parallel (FSDP).

    Here’s a look at how I actually initialize a large model across both 4080s. This isn’t just about running code; it’s about understanding how tensors are distributed across the PCIe bus.

    Python

    import torch
    import torch.nn as nn
    from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
    from torch.distributed.fsdp.fully_sharded_data_parallel import CPUOffload, BackwardPrefetch
    
    def setup_sharded_model(model):
        """
        My approach to fitting massive architectures into dual 4080s.
        We shard the model parameters, gradients, and optimizer states.
        """
        if not torch.cuda.is_available() or torch.cuda.device_count() < 2:
            print("This rig requires dual GPUs for the full sharding experience.")
            return model
    
        # Initializing the model on the primary device
        model = model.to(0)
    
        # Wrap the model in FSDP. This is where the 'math' of 
        # memory management happens.
        sharded_model = FSDP(
            model,
            device_id=torch.cuda.current_device(),
            # Offload to 64GB System RAM if we hit the 32GB VRAM limit
            cpu_offload=CPUOffload(offload_params=True),
            backward_prefetch=BackwardPrefetch.BACKWARD_PRE,
            # Using mixed precision (BF16) to squeeze more out of the 4080s
            mixed_precision=torch.distributed.fsdp.MixedPrecision(
                param_dtype=torch.bfloat16, 
                reduce_dtype=torch.bfloat16, 
                buffer_dtype=torch.bfloat16
            )
        )
        return sharded_model
    
    # Example of a custom training step focusing on manual gradient scaling
    def research_train_step(model, batch, optimizer):
        optimizer.zero_grad()
        
        # Moving data to the GPU (The IO bottleneck I always talk about)
        inputs, labels = batch[0].to(0), batch[1].to(0)
        
        outputs = model(inputs)
        loss = nn.functional.cross_entropy(outputs, labels)
        
        # In a dual-GPU setup, backward() triggers the NCCL sync
        loss.backward()
        optimizer.step()
        
        return loss.item()
    

    The “Unsung Heroes”: CPU and Data Throughput

    While everyone stares at the GPUs, I spent a significant portion of my budget on the 10+ Core CPU and 64GB of RAM (which I’ll likely scale to 128GB soon).

    In my mathematics-heavy workflow, the CPU isn’t just a passenger. It handles the Stochastic Preprocessing. If your CPU can’t keep up with the data augmentation—shuffling tensors, normalizing images, or tokenizing text—your expensive GPUs will just sit there “starving.” This is a classic optimization problem.

    I use an M.2 NVMe SSD (2TB) for active research and a 6TB HDD for long-term storage of model weights. When you’re downloading checkpoints from HuggingFace daily, you’d be surprised how fast 2TB disappears.


    Local Research vs. The Black Box

    There is a certain “intellectual transparency” that comes with local hardware. When I’m on the ground at a conference, and someone claims a new optimization technique, I don’t want to wait to “get home and log into AWS.” I want to know that my rig is ready for a 48-hour training run the moment I land.

    Running on Ubuntu also allows me to use Docker to create reproducible environments. If I’m reconstructing a model from a paper, I can lock down the library versions exactly as the authors intended.

    Python

    # A quick utility I use to monitor VRAM pressure across my dual setup
    def print_gpu_utilization():
        import nvidia_smi
        nvidia_smi.init()
        for i in range(torch.cuda.device_count()):
            handle = nvidia_smi.nvmlDeviceGetHandleByIndex(i)
            info = nvidia_smi.nvmlDeviceGetMemoryInfo(handle)
            print(f"GPU {i} [{nvidia_smi.nvmlDeviceGetName(handle)}]:")
            print(f"  Used: {info.used / 1024**2:.2f} MB / {info.total / 1024**2:.2f} MB")
    
    # If you see one GPU at 99% and the other at 10%, you've failed the 
    # implementation-first test. Balance is everything.
    

    Final Thoughts: The Power of Independence

    This setup—the dual 4080s, the high-performance CPU, the Ubuntu “bare metal”—is my laboratory. It’s where I dismantle the hype and look for the truth in the equations.

    Is it overkill for most people? Probably. But if you’re driven by the mechanics of intelligence, and you want to see the “why” behind the “how,” you need to own your compute. Don’t just read the paper. Shard the model. Run the loss curves. Break the code until you understand the proof.

    That’s how I move the frontier forward—one epoch at a time.