Artificial Intelligence Best Practices: A Complete Guide

INCIDENT POST-MORTEM REPORT #RC-9902
STATUS: CRITICAL / RESOLVED (FOR NOW)
DATE: OCTOBER 24, 2023
LEAD SRE: ELIAS (SRE-01)
DURATION: 18 HOURS, 14 MINUTES
SUBJECT: THE DAY THE WEIGHTS MELTED


I. RAW TERMINAL LOG DUMP (STDOUT/STDERR)

[2023-10-24 03:12:01] [INFO] Starting Smart-Optimizer-v2.sh...
[2023-10-24 03:12:05] [DEBUG] Scanning GPU memory... 8x NVIDIA A100-SXM4-80GB detected.
[2023-10-24 03:12:10] [WARN] CUDA 12.1 context initialized. Memory fragmentation at 14%.
[2023-10-24 03:14:22] [ERROR] torch.cuda.OutOfMemoryError: Tried to allocate 12.50 GiB (GPU 0; 79.35 GiB total capacity; 64.12 GiB already allocated; 10.22 GiB free; 65.01 GiB reserved in total by PyTorch)
[2023-10-24 03:14:22] [CRITICAL] Kernel PID 4402 (python3.11) killed by OOM-Killer.
[2023-10-24 03:14:23] [SYSTEM] Node k8s-gpu-node-04 status: NotReady.
[2023-10-24 03:14:25] [NET] Ingress-Nginx: 502 Bad Gateway (Upstream: 10.244.1.12:8080)
[2023-10-24 03:14:30] [MONITOR] Alert: p99 Latency > 45000ms.
[2023-10-24 03:15:00] [SYSTEM] Cascading failure detected. Sharding logic failing on nodes 01-08.
[2023-10-24 03:15:12] [CRITICAL] Vector database connection refused. Embedding layer offline.
[2023-10-24 03:15:45] [DEBUG] Attempting automated rollback... FAILED. Rollback script requires Python 3.10, current environment is 3.11.4.
[2023-10-24 03:16:00] [FATAL] Total cluster blackout. 100% packet loss on inference API.

II. SUMMARY OF THE DISASTER

I am writing this through a haze of three double-espressos and the smell of ozone that I’m fairly certain is coming from my own brain. My ‘A’ key is missing its cap, so I’m hitting the bare switch. It’s fitting. Everything is bare today.

At 03:12 UTC, a “smart” optimization script, written by someone who clearly thinks artificial intelligence is a magical genie that lives in a silicon lamp, was executed on the production inference cluster. This script was designed to “dynamically re-quantize” our weights from FP16 to INT8 on-the-fly to “save costs.”

The script was built for Python 3.11.4, utilizing PyTorch 2.1.0 and CUDA 12.1. On paper, this is a modern stack. In reality, it was a suicide pact. The script didn’t account for the way CUDA 12.1 handles memory fragmentation when dealing with large-scale vector embeddings during a sharded inference pass. It triggered a massive memory leak, which led to an OOM (Out of Memory) cascade. Because our load balancer was configured to “fail open” (don’t ask me why, I didn’t write the YAML), it kept shoveling traffic into the dying nodes, which then died harder.

The result? Eight A100s turned into very expensive space heaters. The p99 latency didn’t just spike; it left the atmosphere. We went from 120ms to “the request will be finished when the sun expands into a red dwarf.”

The “magic” software people keep talking about? It’s just math. And when the math is wrong, the servers melt. There is no “intelligence” here—only a series of increasingly desperate attempts to keep a house of cards from blowing over in a light breeze.


III. THE CASCADING FAILURE OF THE INFERENCE LAYER

The failure started in the quantization logic. When you move from FP16 to INT8, you’re essentially trying to fit a gallon of water into a pint glass. If you don’t do it with surgical precision, you get “drift.” In this case, the drift detection was nonexistent. The model started outputting garbage—NaNs (Not a Number) everywhere.

When the inference engine (PyTorch 2.1.0) encountered these NaNs during the sharding process, it didn’t just error out. It tried to re-calculate the embeddings. This caused a recursive memory allocation loop. Python 3.11.4 is fast, sure, but it’s not fast enough to outrun a recursive OOM kill.

The sharding logic, which splits the model across eight GPUs, lost synchronization. One GPU would be waiting for a tensor that didn’t exist because its neighbor was already dead. This created a “zombie” state where the processes were still alive but doing nothing but consuming 100% CPU while waiting for a NCCL (NVIDIA Collective Communications Library) timeout that was set to—get this—30 minutes.

Here is the broken YAML that allowed this nightmare to persist. Look at the resource limits. Or rather, the lack of them.

BROKEN CONFIGURATION (k8s-inference-deploy.yaml):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: inference-engine-v2
spec:
  replicas: 8
  template:
    spec:
      containers:
      - name: pytorch-inference
        image: internal-registry/inference:latest-3.11.4-cuda12.1
        resources:
          # This is where the "magic" happens. 
          # No hard limits on memory, just "requests."
          requests:
            nvidia.com/gpu: 1
            memory: "64Gi"
          # limits: 
          #   memory: "80Gi"  <-- Commented out by a "senior" dev to "avoid unnecessary kills"
        env:
        - name: TORCH_CUDA_ARCH_LIST
          value: "8.0"
        - name: OPTIMIZE_ON_FLY
          value: "true" # The trigger for the disaster

By commenting out the limits, the developer essentially told the Linux kernel: “Feel free to let this process eat the entire node until the hardware screams.” And it did.


IV. THE DEPENDENCY ABYSS: PYTHON, PYTORCH, AND CUDA

We need to talk about the “dependency hell” that is modern artificial intelligence development. We are currently pinned to Python 3.11.4. Why? Because 3.12 broke the specific version of the vector database driver we use. We are pinned to PyTorch 2.1.0 because 2.2.0 has a regression in the way it handles torch.compile for Transformer architectures. We are on CUDA 12.1 because the drivers on the host machines are managed by a different team that thinks updating a driver is a “quarterly event.”

When you mix these specific versions, you aren’t building a platform; you’re performing alchemy.

The “Smart-Optimizer” script tried to use a feature in PyTorch 2.1.0 that was supposedly “stable” but had a known conflict with CUDA 12.1’s memory allocator. Specifically, when the script attempted to shard the vector embeddings across the NVLink interconnect, it triggered a race condition.

The cold starts were the final nail. When we tried to bring the nodes back up, the model weights (all 175GB of them) had to be pulled from an S3 bucket. Because the entire cluster was trying to pull at once, we saturated the NAT gateway. 18 hours. It took 18 hours because we spent 4 of them just waiting for bytes to move across a wire.


V. MANIFESTO OF HARD-WON LESSONS

If I see one more LinkedIn post about how “easy” it is to deploy these models, I am going to throw my Model M keyboard through a window. Here is the reality of SRE work in the age of large-scale models. These are not “best practices.” These are survival tactics.

1. Observability is Not Optional

You cannot monitor a model like you monitor a web server. A web server returns a 200 or a 500. A model can return a 200 OK while actually outputting a “hallucination” or a string of NaNs that crashes the downstream service. You need drift detection at the embedding level. You need to monitor the p99 latency of the individual GPU kernels, not just the API endpoint.

2. The Myth of “Smart” Automation

Stop letting scripts make decisions about hardware utilization. If you want to quantize a model, do it in the CI/CD pipeline. Test it. Benchmark it. Verify the loss in precision. Do not—under any circumstances—let a script “optimize” weights on a live production node. “Smart” is just another word for “unpredictable.”

3. Memory is a Finite Resource

GPU memory fragmentation is the silent killer. In PyTorch 2.1.0, the caching allocator is aggressive. If you don’t manually manage the cache or set PYTORCH_CUDA_ALLOC_CONF, you will eventually hit a wall where you have 10GB free but can’t allocate a 1GB contiguous block.

4. Version Pinning is a Blood Oath

You don’t “upgrade” a stack like this. You rebuild it from the ground up and test it for a month. Python 3.11.4 to 3.11.5 might sound minor, but in the world of C-extensions and CUDA kernels, it’s a potential landmine.


VI. THE REMEDIATION: CODE AND LOGIC

To prevent this from happening again, I’ve implemented a mandatory logging decorator that every inference call must use. It doesn’t just log “success.” It tracks the tensor health and memory delta. If it detects a NaN or a sudden 10% jump in memory usage, it kills the process immediately before it can contaminate the rest of the cluster.

FIXED LOGGING DECORATOR (telemetry.py):

import torch
import functools
import logging
from time import perf_counter

logger = logging.getLogger("SRE-Sentinel")

def monitor_inference(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if not torch.cuda.is_available():
            raise RuntimeError("CUDA not available. SRE-Sentinel blocking execution.")

        start_mem = torch.cuda.memory_allocated()
        start_time = perf_counter()

        try:
            result = func(*args, **kwargs)

            # Check for "Garbage Weights" (NaNs)
            if torch.isnan(result).any():
                logger.error("CRITICAL: NaN detected in output tensor. Terminating.")
                raise ValueError("Inference produced NaNs. Potential weight corruption.")

            end_time = perf_counter()
            end_mem = torch.cuda.memory_allocated()

            latency = (end_time - start_time) * 1000
            mem_delta = (end_mem - start_mem) / 1024 / 1024 # MB

            logger.info(f"Inference complete. Latency: {latency:.2f}ms | MemDelta: {mem_delta:.2f}MB")
            return result

        except Exception as e:
            logger.critical(f"Inference failure: {str(e)}")
            # Force clear cache on failure to prevent fragmentation
            torch.cuda.empty_cache()
            raise e

    return wrapper

Furthermore, I have updated the Prometheus rules. We are no longer looking at “average” CPU usage. We are looking at the “Stall Ratio”—the amount of time the GPU is waiting for the CPU to feed it data. If the stall ratio exceeds 15% for more than 2 minutes, we trigger a PagerDuty alert.

PROMETHEUS ALERT RULE (gpu-alerts.rules):

groups:
- name: AI_Inference_Alerts
  rules:
  - alert: HighGPUMemoryFragmentation
    expr: (sum(torch_cuda_memory_reserved_bytes) - sum(torch_cuda_memory_allocated_bytes)) / sum(torch_cuda_memory_reserved_bytes) > 0.3
    for: 5m
    labels:
      severity: critical
    annotations:
      summary: "GPU Memory Fragmentation too high on {{ $labels.instance }}"
      description: "Fragmentation is above 30%. OOM imminent. Python 3.11.4 allocator failing."

  - alert: LatencyP99Spike
    expr: histogram_quantile(0.99, sum(rate(inference_latency_seconds_bucket[5m])) by (le)) > 2.0
    for: 1m
    labels:
      severity: warning
    annotations:
      summary: "p99 Latency exceeds 2 seconds."

VII. THE REALITY OF GPU MEMORY AND SHARDING

Let’s talk about sharding. Everyone thinks sharding is just “splitting the model.” It’s not. It’s a high-speed game of hot potato played with 80GB tensors. When we shard a model across eight A100s, we are using Tensor Parallelism. Every single layer of the model is split. This means for every single forward pass, the GPUs have to talk to each other multiple times.

In our case, the “Smart-Optimizer” script messed with the memory alignment of the shards. When PyTorch 2.1.0 tried to perform an all-reduce operation (combining the results from all GPUs), it found that the tensors were no longer aligned in memory.

This is where the “dependency hell” becomes a literal hell. CUDA 12.1 introduced a new memory management feature that was supposed to make this faster. Instead, it interacted with Python 3.11.4’s new garbage collector in a way that caused the “zombie” processes I mentioned earlier. The garbage collector tried to free a tensor that the GPU was still using for an all-reduce operation.

The result was a kernel panic—not in the OS, but in the CUDA driver itself. Once the driver panics, the only way out is a hard reboot of the physical host. Do you know how long it takes to reboot a server with 2TB of RAM and 8 GPUs? 15 minutes. 15 minutes of staring at a black screen, praying the BIOS doesn’t hang.


VIII. THE HUMAN ELEMENT: “MAGIC” VS. ENGINEERING

The core of the problem isn’t the code. It’s the mindset. Management treats artificial intelligence like a utility—like electricity or water. You turn the tap, and the “intelligence” flows out. They don’t want to hear about CUDA versions or memory fragmentation. They want “seamless” (a word I hate) optimization.

But there is nothing “seamless” about running 175-billion-parameter models. It is a gritty, manual, and often violent process of forcing hardware to do things it wasn’t designed to do. We are using GPUs—chips designed to draw triangles for video games—to simulate the neural pathways of a brain. It’s a hack. The whole industry is a hack.

When someone says “the model is smart,” they are lying. The model is a file full of numbers. If those numbers are INT8 instead of FP16, and you didn’t calculate the scale factors correctly, the “smart” model will tell you that the capital of France is “3.14159.”

We spent 18 hours fixing a problem that was caused by the desire to save a few dollars on compute costs through “smart” automation. We lost more money in downtime in the first ten minutes than that script would have saved us in a decade.


IX. FINAL RECOMMENDATION AND SIGN-OFF

The cluster is back online. The “Smart-Optimizer” script has been deleted, purged from Git history, and the person who wrote it has been banned from touching the production environment until they can explain the difference between a row-major and a column-major matrix.

We are staying on Python 3.11.4 and PyTorch 2.1.0 for now, but we are implementing a strict “No Dynamic Quantization” policy. If you want to change the weights, you do it in a lab, not in a live environment.

HARDWARE RECOMMENDATION:
For the next expansion of the inference rack, do not buy the standard 4U chassis. We need the Supermicro AS-4124GS-TNR. It has the airflow capacity to handle the thermal spikes we saw during the OOM cascade. If the software is going to melt, we might as well have fans that can blow the smoke out of the room before the fire alarm goes off.

I’m going home. If anyone pings me on Slack before noon, I will delete their SSH keys.

Elias
Lead SRE, Inference Operations
Sent from a keyboard with a broken ‘A’ key and a very tired soul.


END OF REPORT

Related Articles

Explore more insights and best practices:

Leave a Comment