Artificial Intelligence Best Practices – Guide

It’s 4:14 AM. The pager went off three days ago and I haven’t seen sunlight since. If I see one more ‘hallucination’ treated as a feature, I’m quitting.

The following is the internal post-mortem for the “Project Chimera” collapse that wiped out our production inference cluster across three regions. If you are a junior dev looking for a “quick fix,” go back to Stack Overflow. This is for the people who have to clean up the blood.

I. INCIDENT LOG: TRACEBACKS AND KERNEL PANICS

The failure started at 02:14 UTC on Tuesday. It wasn’t a sudden crash; it was a slow, agonizing degradation of the inference service that eventually cascaded into a total cluster lockout.

# Extract from /var/log/k8s/inference-pod-a7xf2.log
[2023-10-24 02:14:08] INFO: Starting inference request 0x9f22
[2023-10-24 02:14:10] ERROR: torch.cuda.OutOfMemoryError: CUDA out of memory. Tried to allocate 512.00 MiB (GPU 0; 40.00 GiB total capacity; 38.21 GiB already allocated; 128.00 MiB free; 38.50 GiB reserved in total by PyTorch) If reserved memory is >> allocated memory try setting max_split_size_mb to avoid fragmentation.
[2023-10-24 02:14:10] CRITICAL: Service terminated with exit code 139 (Segmentation Fault)
[2023-10-24 02:14:12] DEBUG: K8s Liveness probe failed. Restarting container...
[2023-10-24 02:14:45] WARN: Node gke-gpu-pool-92bc-x1 is under heavy memory pressure.
[2023-10-24 02:15:01] FATAL: kernel: [192834.12] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/kubepods/besteffort/pod...,task=python3,pid=4122,uid=1000

I ran a quick grep across the logs to see how widespread this was. It was everywhere.

# Checking for the frequency of OOM kills across the namespace
kubectl logs -n ml-prod -l app=inference-engine --tail=10000 | grep -c "OutOfMemoryError"
> 4,821

# Checking the status of the vector DB sidecar
kubectl get pods -n ml-prod | grep "vector-db-proxy" | awk '{print $3}' | uniq -c
> 142 CrashLoopBackOff
> 12 Running

The system was eating itself. The “artificial intelligence” we spent six months building turned into a recursive loop of garbage data that bloated the KV cache until the GPUs choked.

II. ROOT CAUSE ANALYSIS: THE DRIFT OF THE UNMONITORED STOCHASTIC PARROT

The marketing department calls this “artificial intelligence,” but let’s be honest: it’s a fragile stack of Python libraries, C++ bindings, and unoptimized CUDA kernels held together by hope.

The root cause was a silent failure in our data ingestion pipeline. We are running Transformers 4.38.0 on PyTorch 2.2.1 with CUDA 12.1. Last Friday, the upstream “User Feedback” schema was modified without a corresponding update to our preprocessing script. Specifically, a field that used to be a truncated string was replaced with a raw, multi-megabyte JSON blob.

Our model, a quantized 4-bit Llama-2-70b variant running via bitsandbytes, wasn’t configured with hard token limits on the input side. It tried to ingest the entire JSON blob into the context window. Because we were using flash-attention-2, the memory growth was supposed to be linear, but the sheer volume of concurrent requests with 32k+ tokens caused the GPU memory fragmentation to skyrocket.

Furthermore, we encountered a massive “model drift” issue. The “artificial intelligence” started generating repetitive, high-entropy tokens because the input distribution shifted so far from the training set. These high-entropy outputs caused the vLLM scheduler to miscalculate the required block allocation in the PagedAttention mechanism, leading to the segmentation faults seen in the logs.

We treated the model as a black box. We didn’t monitor the Kolmogorov-Smirnov test results for our input features. We didn’t have a circuit breaker for token counts. We just assumed the “intelligence” would handle it. It didn’t. It just died.

III. VECTOR DATABASE OPTIMIZATION: BEYOND THE DEFAULT INDEX

We are using Milvus 2.3.10 for our vector embeddings. The outage was exacerbated by the fact that our vector search latency went from 40ms to 12s. Why? Because someone thought it was a good idea to use the default HNSW (Hierarchical Navigable Small World) parameters without considering the scale of our metadata.

When the model drift occurred, the queries being sent to Milvus became increasingly nonsensical, hitting cold areas of the index.

Hard-Learned Rule #1: Tune your HNSW Parameters

Stop using the defaults. For a production-grade Milvus or Pinecone deployment, you must explicitly define your M (max degree of the node) and efConstruction (search scope during index construction).

# Example of a sane Milvus index configuration
index_params = {
    "metric_type": "IP", # Inner Product for normalized embeddings
    "index_type": "HNSW",
    "params": {
        "M": 16,             # Range 4-64. Higher = more memory, better accuracy
        "efConstruction": 128 # Range 8-512. Higher = slower build, better search
    }
}

If you are using Pinecone, ensure you are not saturating your pod’s IOPS by sending massive metadata payloads. We found that stripping the metadata and storing it in a sidecar Redis instance (version 7.2.4) reduced our vector search latency by 60%. The “artificial intelligence” doesn’t need to see the raw JSON; it only needs the vector ID and the pre-sanitized context.

Hard-Learned Rule #2: Index Compaction and Garbage Collection

Milvus doesn’t just “handle” deleted vectors. If you are constantly updating embeddings due to model retraining or drift correction, you must trigger manual compactions. We had 40% “tombstone” data in our segments, which forced the search engine to scan dead memory.

# Manual compaction trigger via Milvus CLI
milvus_cli > compact -c collection_chimera_v2

IV. QUANTIZATION STRATEGIES AND GPU MEMORY FRAGMENTATION

We were running 4-bit quantization using bitsandbytes to save on VRAM. This is a trap if you don’t understand how NF4 (NormalFloat 4) works. While it reduces the model footprint, the dequantization step during the forward pass happens in FP16 or BF16.

If your PYTORCH_CUDA_ALLOC_CONF is not tuned, PyTorch will keep hold of these intermediate buffers, leading to the “CUDA Out of Memory” errors we saw.

The 4-bit vs 8-bit Tradeoff

In our post-mortem testing, we found that 8-bit quantization (int8) is significantly more stable for long-running inference services than 4-bit. The 4-bit weights are too sensitive to high-variance inputs. If you must use 4-bit, you need to implement a strict max_new_tokens limit and a max_input_length validator at the API gateway level.

GPU Memory Management

Add this to your environment variables immediately. Do not pass go. Do not collect $200.

export PYTORCH_CUDA_ALLOC_CONF="max_split_size_mb:128,garbage_collection_threshold:0.8"

This forces PyTorch to be more aggressive about releasing memory blocks. Without this, the “artificial intelligence” will eventually fragment your VRAM until even a small 512-token request fails.

V. DATA SANITIZATION AND THE FALLACY OF THE RAW PROMPT

The most embarrassing part of this outage was the lack of basic input validation. We allowed raw user input to be concatenated into a f-string and sent to the model. This isn’t just a security risk (prompt injection); it’s a stability nightmare.

The Sanitization Pipeline

Every input must pass through a multi-stage pipeline before it touches the Transformers tokenizer:
1. Length Validation: Reject anything over a hard character limit.
2. Regex Scrubbing: Remove non-printable characters and excessive whitespace that can confuse the tokenizer’s BPE (Byte Pair Encoding) logic.
3. Schema Enforcement: Use Pydantic (version 2.6.1) to ensure the input matches the expected structure.

from pydantic import BaseModel, Field, validator

class InferenceRequest(BaseModel):
    prompt: str = Field(..., max_length=4096)
    temperature: float = Field(0.7, ge=0.0, le=1.5)

    @validator('prompt')
    def no_garbage_tokens(cls, v):
        if "[[REDACTED_INTERNAL_KEY]]" in v:
            raise ValueError("Potential leak detected")
        return v.strip()

If the “artificial intelligence” receives garbage, it produces garbage. If it produces garbage at 100 requests per second, it crashes the cluster.

VI. OBSERVABILITY: METRICS THAT ACTUALLY MEAN SOMETHING

Our Grafana dashboards were full of useless metrics like “CPU Usage” and “Disk I/O.” These are irrelevant for LLM ops. You need to monitor the internals of the model’s behavior.

Critical Metrics to Track:

  1. KV Cache Utilization: If this hits 90%, start dropping requests.
  2. Token Throughput (Tokens/Sec): A sudden drop indicates bottlenecking in the attention kernels.
  3. Per-Token Latency (P99): Not just total request latency. You need to know how long each token takes to generate to identify “stuck” sequences.
  4. Output Entropy: This is the most important metric for detecting model drift. If the average log-probability of the generated tokens starts to diverge significantly from your baseline, the model is “hallucinating” or looping.

Prometheus Configuration for vLLM

If you are using vLLM 0.3.3, ensure you are scraping the /metrics endpoint and looking for vllm:avg_generation_throughput.

# prometheus-config.yaml snippet
- job_name: 'vllm-inference'
  static_configs:
    - targets: ['inference-service.ml-prod.svc.cluster.local:8000']
  metrics_path: /metrics
  relabel_configs:
    - source_labels: [__address__]
      target_label: instance

VII. RATE LIMITING AND THE “CIRCUIT BREAKER” PATTERN

We failed because we didn’t have backpressure. When the inference service slowed down, the upstream services just queued more requests, leading to a classic thundering herd problem.

You must implement a token-bucket rate limiter that is aware of the token count, not just the request count. A single request with 4,000 tokens is more expensive than 40 requests with 10 tokens.

Docker Compose for a Resilient Inference Node

This is how the node should have been configured. Note the resource limits and the health checks that actually check the model’s responsiveness, not just the HTTP port.

version: '3.8'
services:
  inference-engine:
    image: vllm/vllm-openai:v0.3.3
    command: >
      --model /models/llama-2-70b-hf
      --quantization bitsandbytes
      --max-model-len 8192
      --gpu-memory-utilization 0.90
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              device_ids: ['0']
              capabilities: [gpu]
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:8000/v1/models || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
    environment:
      - CUDA_VISIBLE_DEVICES=0
      - PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:128

VIII. THE FALLACY OF “AUTO-SCALING” AI

K8s Horizontal Pod Autoscaler (HPA) is useless for “artificial intelligence” workloads if you base it on CPU or Memory. By the time the memory usage spikes, the GPU is already OOM.

You need to scale based on Available KV Cache Slots. We are now writing a custom metrics adapter that queries the vLLM engine for vllm:num_requests_running. If the number of running requests exceeds 80% of our total slot capacity, we spin up a new node. If there are no more nodes available, we return a 503 Service Unavailable with a Retry-After header. It is better to fail gracefully than to take down the entire cluster.

IX. MODEL VERSIONING AND SHADOW DEPLOYMENTS

We pushed the updated quantization config directly to production. This was a mistake. From now on, no model change—not even a change in the temperature parameter—goes live without a 24-hour “Shadow Deployment.”

In a shadow deployment, the production traffic is mirrored to the new model version. We compare the outputs. If the Kullback-Leibler (KL) divergence between the production model and the shadow model exceeds a threshold, the deployment is automatically aborted.

# Example of mirroring traffic using Istio
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: inference-mirror
spec:
  hosts:
    - inference-service
  http:
  - route:
    - destination:
        host: inference-service-v1
      weight: 100
    mirror:
      host: inference-service-v2-shadow
    mirror_percentage:
      value: 10.0

X. CONCLUSION: STOP TREATING IT LIKE MAGIC

The failure of this “artificial intelligence” implementation was entirely predictable given the lack of input sanitization and the disregard for low-level GPU memory management. We treated the model like a web server. It isn’t. It’s a high-performance computing workload that is extremely sensitive to input variance.

If you are going to work on this stack, you need to understand the difference between FP16 and BF16. You need to know why PagedAttention is better than standard attention. You need to be able to read a CUDA traceback.

I’m going to sleep for four hours. If the pager goes off because someone changed a hyperparameter in production without testing it, don’t bother calling me. Just start updating your resume.

Document Version: 1.0.4
Author: SRE Lead (Cluster 4)
Status: Exhausted.
Approved for internal distribution only.

Related Articles

Explore more insights and best practices:

Leave a Comment