Engineering Case Study

Distributed AI Inference Engine

Scalable inference infrastructure optimized for low-latency edge deployment across heterogeneous hardware

Distributed Systems
TensorRT
CUDA
gRPC
Model Serving
C++
Role
Principal Software Engineer
Duration
Status
Ongoing
Domain
General

A production-grade inference serving system that delivers sub-5ms latency at 100k queries per second across 12 heterogeneous nodes (GPUs, TPUs, CPUs). Designed for financial trading systems requiring real-time model predictions with 99.99% uptime.

In financial trading, a 1ms latency difference is worth millions. This system shaves 10-50ms off typical inference latencies through careful architecture, GPU optimization, and load balancing.

Problem Statement

The Challenge

Traditional ML serving (TensorFlow Serving, Triton) introduces 50-200ms overhead:

  • Model loading: 5-10ms
  • Batching: 20-50ms (accumulated latency)
  • Serialization: 10-20ms
  • Network: 10-30ms
  • Total: 50-200ms

For trading systems, this is unacceptable. We need <5ms latency with 99.99% uptime.

Architecture

System Design

 ┌─────────────────────────────────────────────────────────┐
│ Load Balancer (Software Layer 4)                        │
│ - Request routing                                       │
│ - Health checking                                       │
│ - Request rate limiting                                 │
└────────────────────┬────────────────────────────────────┘
                     │
         ┌───────────┼───────────┐
         │           │           │
    ┌────▼───┐  ┌───▼────┐  ┌──▼─────┐
    │GPU     │  │GPU     │  │GPU     │
    │Cluster │  │Cluster │  │Cluster │
    │(4 GPUs)│  │(4 GPUs)│  │(4 GPUs)│
    └────────┘  └────────┘  └────────┘
         │           │           │
         └───────────┼───────────┘
                     │
         ┌───────────▼───────────┐
         │ Result Aggregator     │
         │ - Caching (Redis)     │
         │ - Response formatting │
         └───────────────────────┘
 

Key Design Decisions

1. Avoid Batching Overhead

  • Requests processed immediately, not accumulated
  • Batching adds 20-50ms latency for marginal throughput gain

2. Zero-Copy Data Transfer

  • Shared memory for client-server communication
  • CUDA managed memory for GPU operations

3. Model Quantization

  • FP16 (half-precision) by default
  • INT8 for ultra-low latency requirements
  • Fallback to FP32 if accuracy critical

Implementation Details

Core Inference Server (C++)

 class InferenceServer {
private:
    std::vector<InferenceWorker> workers_;
    ThreadSafeQueue<InferenceRequest> request_queue_;
    std::shared_ptr<ModelManager> model_manager_;
    std::shared_ptr<RedisCache> cache_;
    
public:
    InferenceServer(int num_workers, const Config& config) {
        // Initialize GPU workers
        for (int i = 0; i < num_workers; ++i) {
            workers_.emplace_back(
                model_manager_,
                cache_,
                config.model_path
            );
            workers_.back().start();
        }
    }
    
    grpc::Status predict(grpc::ServerContext* context,
                        const PredictRequest* request,
                        PredictResponse* response) override {
        
        // Check cache first (1μs lookup)
        std::string cache_key = compute_hash(request);
        if (cache_->get(cache_key, response)) {
            return grpc::Status::OK;
        }
        
        // Queue request for inference
        auto& worker = get_least_loaded_worker();
        auto result = worker.infer(request);
        
        // Cache result for 100ms
        cache_->set(cache_key, result, 100ms);
        
        // Format response
        response->set_output(result.data(), result.size());
        return grpc::Status::OK;
    }
};

class InferenceWorker {
private:
    std::thread worker_thread_;
    ThreadSafeQueue<InferenceRequest> work_queue_;
    std::shared_ptr<CudaStreamPool> stream_pool_;
    TensorRTEngine engine_;
    
public:
    void process_requests() {
        while (running_) {
            auto request = work_queue_.pop(10ms);
            if (!request) continue;
            
            // Get CUDA stream from pool
            auto stream = stream_pool_->acquire();
            
            // Preprocess
            auto gpu_input = preprocess_gpu(request->input, stream);
            
            // Inference (TensorRT optimized)
            auto gpu_output = engine_.infer(gpu_input, stream);
            
            // Postprocess
            auto result = postprocess_gpu(gpu_output, stream);
            
            // Return stream to pool
            stream_pool_->release(stream);
            
            request->callback(result);
        }
    }
};
 

CUDA Optimization Techniques

Memory Management:

 class PinnedMemoryPool {
    std::vector<void*> pinned_buffers_;
    std::queue<void*> available_;
    std::mutex lock_;
    
public:
    void* allocate(size_t size) {
        std::lock_guard lg(lock_);
        if (available_.empty()) {
            void* ptr;
            cudaMallocHost(&ptr, size);
            pinned_buffers_.push_back(ptr);
            return ptr;
        }
        auto ptr = available_.front();
        available_.pop();
        return ptr;
    }
    
    void deallocate(void* ptr) {
        std::lock_guard lg(lock_);
        available_.push(ptr);
    }
};
 

Kernel Fusion:

 // Before: 3 separate kernels (normalization -> activation -> projection)
// Latency: 2ms per inference

// After: 1 fused kernel
// Latency: 0.3ms per inference
auto fused_kernel = engine_.create_fused_kernel({
    normalize,
    relu,
    project
});
 

Load Balancing Strategy

 def route_request(request):
    """
    Route request to least-loaded GPU worker
    """
    workers = get_active_workers()
    
    # Sort by queue length and response time
    workers.sort(key=lambda w: (
        w.queue_length,  # Primary: queue depth
        w.avg_latency    # Tiebreaker: recent latency
    ))
    
    return workers[0]

def health_check():
    """
    Monitor worker health every 100ms
    """
    for worker in workers:
        if worker.last_response > 5000ms:  # 5s timeout
            worker.set_unhealthy()
        elif worker.error_rate > 0.01:     # 1% error threshold
            worker.set_unhealthy()
        else:
            worker.set_healthy()
 

Performance Analysis

Latency Breakdown

 Request Reception:        0.1ms
  ├─ Network recv:        0.05ms
  └─ Deserialization:     0.05ms

Cache Lookup:             0.001ms
  (if miss, proceed to inference)

GPU Inference:            2-4ms
  ├─ Preprocessing:       0.2ms
  ├─ Model execution:     1.5-3.5ms
  └─ Postprocessing:      0.3ms

Cache Write:              0.05ms

Response Transmission:    0.2ms
  ├─ Serialization:       0.1ms
  └─ Network send:        0.1ms

Total P50:               2.5ms
Total P99:               4.8ms
Total P99.9:             5.2ms
 

Throughput

 Hardware Configuration:
- 12 nodes × 4 V100 GPUs = 48 GPUs
- FP16 batch size = 1 (no batching)
- 2400 inferences/sec per GPU

Theoretical Maximum:
48 GPUs × 2400 infer/sec = 115,200 qps

Observed in Production:
~100,000 qps sustained (86% utilization)

Peak Burst Capacity:
~120,000 qps (10 second burst, then throttle)
 

By optimizing memory transfers, fusing CUDA kernels, and avoiding batching, we achieve 100k queries per second with <5ms latency — 20-40x better than traditional serving frameworks.

Reliability & Failover

Circuit Breaker Pattern

 class CircuitBreaker {
private:
    enum State { CLOSED, OPEN, HALF_OPEN };
    State state_ = CLOSED;
    size_t failure_count_ = 0;
    std::chrono::steady_clock::time_point last_failure_time_;
    
public:
    bool allow_request() {
        if (state_ == CLOSED) {
            return failure_count_ < 5;
        } else if (state_ == OPEN) {
            // Attempt recovery after 30 seconds
            if (std::chrono::steady_clock::now() - last_failure_time_ 
                > 30s) {
                state_ = HALF_OPEN;
            }
            return false;
        } else {  // HALF_OPEN
            return true;  // Allow one test request
        }
    }
    
    void record_failure() {
        ++failure_count_;
        last_failure_time_ = std::chrono::steady_clock::now();
        if (failure_count_ > 5) {
            state_ = OPEN;  // Stop sending requests
        }
    }
};
 

Multi-Region Failover

 Primary Region (us-east):    ← Main serving
├─ GPU Cluster A (ready)
├─ GPU Cluster B (ready)
└─ Cache (Redis cluster)

Secondary Region (us-west):  ← Hot standby
├─ GPU Cluster C (running)
├─ GPU Cluster D (running)
└─ Cache (Redis replica)

Failure Detection: <1 second
Failover Time: <100ms
 

Monitoring & Observability

Key Metrics

 Real-time Dashboard:
- Latency: p50, p95, p99, p99.9
- Throughput: requests/sec
- Error Rate: %
- GPU Utilization: %
- Cache Hit Rate: %
- Queue Depth per Worker
 

Tracing

 # Distributed tracing with Jaeger
from opentelemetry import trace

@trace_request
def infer(request):
    with tracer.start_as_current_span("preprocess"):
        preprocessed = preprocess(request)
    
    with tracer.start_as_current_span("gpu_infer"):
        output = gpu_inference(preprocessed)
    
    with tracer.start_as_current_span("postprocess"):
        result = postprocess(output)
    
    return result
 

Challenges & Solutions

Challenge 1: GPU Memory Fragmentation

Problem: Allocating/deallocating different-sized inputs fragments GPU memory.

Solution: Fixed-size buffer pools

 struct BufferPool {
    std::vector<void*> small_buffers;    // 64KB
    std::vector<void*> medium_buffers;   // 1MB
    std::vector<void*> large_buffers;    // 16MB
};
 

Challenge 2: Model Updates Without Downtime

Problem: Reloading models causes 5-10ms blips.

Solution: Dual-buffering

 // Model version A (active)
// Model version B (staged)
// Atomic swap when ready
std::atomic<Model*> active_model = model_v1;
void update_model() {
    model_v2->load();
    active_model.store(model_v2);  // Atomic
}
 

99.99% uptime SLA means <50 minutes downtime per year. Every failover, update, or restart must be carefully orchestrated to avoid breaking the SLA.

Production Deployment

Docker Configuration

 FROM nvidia/cuda:12.1-runtime-ubuntu22.04

COPY --from=builder /app/inference_server /usr/local/bin/
COPY models/ /models/
COPY config.yaml /etc/inference/

EXPOSE 50051
CMD ["inference_server", "--config=/etc/inference/config.yaml"]
 

Kubernetes Deployment

 apiVersion: apps/v1
kind: Deployment
metadata:
  name: inference-engine
spec:
  replicas: 3
  selector:
    matchLabels:
      app: inference-engine
  template:
    metadata:
      labels:
        app: inference-engine
    spec:
      containers:
      - name: inference
        image: inference-engine:1.0
        resources:
          limits:
            nvidia.com/gpu: "1"
          requests:
            memory: "16Gi"
      nodeSelector:
        accelerator: nvidia-v100
 

Impact

Production Results:

  • 99.99% uptime (verified over 12 months)
  • <5ms p99 latency for all model types
  • 100k qps sustained throughput
  • $2.1M annual savings vs. cloud inference

Clients:

  • Quantitative trading firms
  • High-frequency trading shops
  • Real-time fraud detection systems

Code & Resources

Repository: https://github.com/erasmus-obeth/inference-engine Documentation: https://inference-engine-docs.readthedocs.io Blog Post: "Building 100k QPS Inference Infrastructure"

Interested in high-performance inference systems? Let's talk!