Ecstaticloud
Initializing0%
Ecstaticloud Logo
Back to Insights
AI & Cloud InfrastructureSeptember 3, 2026

Building Production-Grade LLM Pipelines: Fine-Tuning Open-Source Models on Private Kubernetes Clusters

Learn how to deploy Ray and DeepSpeed on private Kubernetes clusters to fine-tune open-source LLMs without compromising data privacy or breaking cloud budgets. This deep dive covers cost-effective infrastructure scaling, GPU orchestration, and zero-downtime model deployment strategies.

Sending sensitive enterprise data to public LLM API endpoints is a non-starter for organizations operating under strict compliance frameworks like HIPAA, GDPR, or SOC 2. However, attempting to fine-tune open-source models—such as Llama 3 or Mistral—on private infrastructure frequently exposes a different class of failures: catastrophic Out-Of-Memory (OOM) errors, severe inter-node communication bottlenecks, uncontrolled cloud expenditure, and brittle training jobs that fail when an instance preemption occurs.

To transition LLM fine-tuning from an experimental notebook exercise into a production-grade infrastructure capability, you need an architecture designed for high throughput, data isolation, and elasticity.

This guide details how to build a enterprise-grade distributed fine-tuning and deployment pipeline on a private Kubernetes cluster using KubeRay, DeepSpeed, and vLLM.


Architectural Overview: The Private AI Stack

A robust Kubernetes-native training architecture separates orchestration, state management, compute execution, and model serving into distinct, decoupled control planes.

+------------------------------------------------------------------+
|                     Ingress / API Gateway                        |
+------------------------------------------------------------------+
                                 |
                                 v
+------------------------------------------------------------------+
|                  Model Serving (vLLM / TGI)                      |
|       [ Kubernetes Deployments + HPA + Dynamic Batching ]        |
+------------------------------------------------------------------+
                                 ^
                                 | (Loads Fine-Tuned Adapter Weights)
+------------------------------------------------------------------+
|                 Shared S3-Compatible Storage                     |
|                 (MinIO / Ceph / Cloud Object Storage)             |
+------------------------------------------------------------------+
                                 ^
                                 | (Persists Checkpoints)
+------------------------------------------------------------------+
|              Distributed Training Engine (KubeRay)               |
|   +---------------------+             +---------------------+    |
|   | Ray Head Node Pod   |------------>| Ray Worker Node Pod |    |
|   | (Orchestration)     |             | (DeepSpeed ZeRO-3)  |    |
|   +---------------------+             +---------------------+    |
+------------------------------------------------------------------+
                                 |
+------------------------------------------------------------------+
|               Kubernetes Physical Compute Pool                   |
|       [ NVIDIA GPU Operator + GPUDirect RDMA + Local NVMe ]       |
+------------------------------------------------------------------+

Core Components

  1. Kubernetes Compute Nodes: Physical or virtual nodes equipped with NVIDIA Tensor Core GPUs (e.g., A100/H100) managed by the NVIDIA GPU Operator.
  2. KubeRay Operator: Manages the lifecycle of Ray clusters declaratively inside Kubernetes, abstracting node provisioning, object storage, and process distribution.
  3. DeepSpeed Optimization Framework: Handles memory partitioning (ZeRO stages), gradient accumulation, and CPU/NVMe offloading.
  4. Shared Object Storage (MinIO/Ceph): Serves as the central repository for base model weights, dataset shards, and training checkpoints.
  5. vLLM Serving Infrastructure: Provides high-throughput, low-latency inference endpoints using PagedAttention to consume fine-tuned weights.

The Math Behind GPU Memory Sizing & DeepSpeed ZeRO

Before launching a distributed cluster, you must calculate the precise VRAM requirement per node to select the right instance types and avoid runtime OOM errors.

The Memory Footprint of Model Fine-Tuning

When fine-tuning a model using full precision or mixed-precision (FP16/BF16) with the Adam optimizer, memory overhead expands far beyond the base parameter count $P$.

For a model with $P$ parameters using 16-bit precision (2 bytes per parameter):

  1. Model Weights: $2 \times P$ bytes
  2. Gradients: $2 \times P$ bytes
  3. Optimizer States (Adam):
    • FP32 Copy of Parameters: $4 \times P$ bytes
    • FP32 Momentum: $4 \times P$ bytes
    • FP32 Variance: $4 \times P$ bytes
      Total Optimizer Footprint = $12 \times P$ bytes

$$\text{Static Memory Footprint} = 2P (\text{weights}) + 2P (\text{grads}) + 12P (\text{optimizer}) = 16P \text{ bytes}$$

Example: Fine-tuning Llama-3-8B ($8 \times 10^9$ parameters): $$\text{Static Footprint} = 16 \times 8 \times 10^9 \text{ bytes} \approx 128 \text{ GB VRAM}$$ Note: This calculation excludes activation memory and KV cache overhead, meaning a single 80GB A100 GPU cannot fine-tune an 8B model natively without optimization.

DeepSpeed Zero Redundancy Optimizer (ZeRO) Memory Reduction Strategies

DeepSpeed eliminates redundant state memory across distributed GPUs by partitioning optimization components:

  • ZeRO-Stage 1: Partitions Adam optimizer states across $N$ data-parallel processes.
    $$\text{Memory Footprint per GPU} = 2P + 2P + \frac{12P}{N}$$
  • ZeRO-Stage 2: Partitions optimizer states and gradients across $N$ processes.
    $$\text{Memory Footprint per GPU} = 2P + \frac{2P + 12P}{N}$$
  • ZeRO-Stage 3: Partitions optimizer states, gradients, and model parameters. Parameters are gathered dynamically via all-gather operations during forward and backward passes and immediately discarded.
    $$\text{Memory Footprint per GPU} = \frac{16P}{N}$$
ZeRO-1: [ Model Weights (2P) ] [ Gradients (2P) ] [ Optimizer State / N ]
ZeRO-2: [ Model Weights (2P) ] [ Gradients / N  ] [ Optimizer State / N ]
ZeRO-3: [ Model Weights / N  ] [ Gradients / N  ] [ Optimizer State / N ]

Provisioning the Ray Cluster on Kubernetes

Using the KubeRay Operator, we construct a declarative cluster topology specifying distinct specs for head and worker pods.

1. Crucial Infrastructure Prerequisite: Shared Memory (/dev/shm)

PyTorch dataloaders use POSIX shared memory to transfer data between processes. Docker's default /dev/shm allocation is 64 MB, which causes instant child process termination (SIGBUS signal) during distributed dataloading. You must mount an emptyDir backed by RAM (medium: Memory) to /dev/shm.

2. RayCluster Custom Resource Manifest

Save the following specification as ray-cluster-finetune.yaml:

apiVersion: ray.io/v1
kind: RayCluster
metadata:
  name: ray-llm-finetune
  namespace: ml-platform
spec:
  rayVersion: '2.35.0'
  headGroupSpec:
    rayStartParams:
      dashboard-host: '0.0.0.0'
      num-gpus: '0' # Keep head node dedicated to orchestration
    template:
      spec:
        nodeSelector:
          node.kubernetes.io/instance-type: cpu-optimized
        containers:
          - name: ray-head
            image: rayproject/ray-ml:2.35.0-py310-gpu
            resources:
              limits:
                cpu: "8"
                memory: "32Gi"
              requests:
                cpu: "4"
                memory: "16Gi"
            volumeMounts:
              - mountPath: /dev/shm
                name: dshm
        volumes:
          - name: dshm
            emptyDir:
              medium: Memory
              sizeLimit: 10Gi
  workerGroupSpecs:
    - groupName: gpu-group
      replicas: 2
      minReplicas: 1
      maxReplicas: 4
      rayStartParams:
        block: 'true'
      template:
        spec:
          nodeSelector:
            accelerator: nvidia-a100
          tolerations:
            - key: "nvidia.com/gpu"
              operator: "Exists"
              effect: "NoSchedule"
          containers:
            - name: ray-worker
              image: rayproject/ray-ml:2.35.0-py310-gpu
              resources:
                limits:
                  cpu: "32"
                  memory: "128Gi"
                  nvidia.com/gpu: "4"
                requests:
                  cpu: "16"
                  memory: "64Gi"
                  nvidia.com/gpu: "4"
              volumeMounts:
                - mountPath: /dev/shm
                  name: dshm
              env:
                - name: NCCL_DEBUG
                  value: "INFO"
                - name: NCCL_IB_DISABLE
                  value: "0" # Set to 1 if InfiniBand is NOT available
          volumes:
            - name: dshm
              emptyDir:
                medium: Memory
                sizeLimit: 64Gi

Apply the configuration:

kubectl apply -f ray-cluster-finetune.yaml -n ml-platform

Implementing the Distributed Fine-Tuning Script

The following Python script leverages ray.train and Hugging Face transformers integrated with DeepSpeed ZeRO-3. It configures fault-tolerant checkpointing directed toward S3-compatible storage.

# finetune_llama.py
import os
import tempfile
import torch
import torch.distributed as dist
from datasets import load_dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    TrainingArguments,
    Trainer,
    DataCollatorForLanguageModeling,
)

import ray
import ray.train
from ray.train import ScalingConfig, Checkpoint
from ray.train.torch import TorchTrainer, TorchConfig

def train_func(config: dict):
    # Retrieve hyperparameters from Ray config
    model_id = config.get("model_id", "meta-llama/Meta-Llama-3-8B")
    epochs = config.get("epochs", 3)
    batch_size = config.get("per_device_batch_size", 2)

    tokenizer = AutoTokenizer.from_pretrained(model_id)
    tokenizer.pad_token = tokenizer.eos_token

    # Load and preprocess sample dataset
    dataset = load_dataset("wikitext", "wikitext-2-raw-v1", split="train")
    
    def tokenize_function(examples):
        return tokenizer(examples["text"], truncation=True, max_length=512)

    tokenized_dataset = dataset.map(tokenize_function, batched=True, remove_columns=["text"])

    # Define DeepSpeed ZeRO-3 Runtime Configuration
    deepspeed_config = {
        "fp16": {"enabled": False},
        "bf16": {"enabled": True},
        "zero_optimization": {
            "stage": 3,
            "offload_optimizer": {
                "device": "cpu",
                "pin_memory": True
            },
            "offload_param": {
                "device": "none"
            },
            "overlap_comm": True,
            "allgather_bucket_size": 5e7,
            "reduce_bucket_size": 5e7
        },
        "gradient_accumulation_steps": 4,
        "train_micro_batch_size_per_gpu": batch_size,
    }

    training_args = TrainingArguments(
        output_dir="./results",
        num_train_epochs=epochs,
        per_device_train_batch_size=batch_size,
        gradient_accumulation_steps=4,
        bf16=True,
        logging_steps=10,
        deepspeed=deepspeed_config,
        save_strategy="epoch",
        report_to="none",
        disable_tqdm=True,
    )

    model = AutoModelForCausalLM.from_pretrained(
        model_id,
        use_cache=False,
        torch_dtype=torch.bfloat16
    )

    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=tokenized_dataset,
        data_collator=DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False),
    )

    # Execute training
    trainer.train()

    # Save artifact state for Ray checkpointing
    with tempfile.TemporaryDirectory() as temp_checkpoint_dir:
        trainer.save_model(temp_checkpoint_dir)
        tokenizer.save_pretrained(temp_checkpoint_dir)
        ray.train.report(
            metrics={"loss": trainer.state.best_metric or 0.0},
            checkpoint=Checkpoint.from_directory(temp_checkpoint_dir),
        )

def run_distributed_job():
    # Initialize Ray runtime connection within the pod
    ray.init()

    scaling_config = ScalingConfig(
        num_workers=8, # Total GPUs across all worker nodes (2 nodes * 4 GPUs)
        use_gpu=True,
        resources_per_worker={"CPU": 4, "GPU": 1},
    )

    trainer = TorchTrainer(
        train_loop_per_worker=train_func,
        train_loop_config={
            "model_id": "meta-llama/Meta-Llama-3-8B",
            "epochs": 1,
            "per_device_batch_size": 2,
        },
        torch_config=TorchConfig(backend="nccl"),
        scaling_config=scaling_config,
        run_config=ray.train.RunConfig(
            name="llama3-deepspeed-finetune",
            storage_path="s3://ml-checkpoints-bucket/ray_results", # Persistent store
        ),
    )

    results = trainer.fit()
    print(f"Training completed successfully. Checkpoint location: {results.checkpoint.path}")

if __name__ == "__main__":
    run_distributed_job()

Infrastructure Optimizations: Network & Storage Bottlenecks

Inter-Node Bandwidth Strategy (NCCL Tuning)

In ZeRO-3, model parameters are constantly gathered across the network during every forward and backward pass. Inter-node network performance is almost always the ultimate bottleneck.

  • NVIDIA GPUDirect RDMA (GDR): Ensure your Kubernetes nodes have GPUDirect installed. This enables GPUs to communicate across host networks via InfiniBand or RoCE (RDMA over Converged Ethernet) bypassing host CPU and RAM.
  • NCCL Environment Variables: Set the following within your Worker Pod container templates to optimize communication topologies:
# Force NCCL to use RoCE interfaces if native InfiniBand isn't present
export NCCL_IB_DISABLE=0
export NCCL_NET_GDR_LEVEL=3
export NCCL_IB_GID_INDEX=3
# Increase buffer size for cross-node parameter collection streams
export NCCL_BUFFSIZE=4194304

Zero-Downtime Serving Architecture with vLLM

Once fine-tuning persists the new base or LoRA adapter weights to your object storage, you need to launch a zero-downtime inference service using vLLM and standard Kubernetes rolling deployments.

vLLM Deployment Manifest

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-llama3-serving
  namespace: ml-platform
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0 # Ensures continuous availability
  selector:
    matchLabels:
      app: vllm-serving
  template:
    metadata:
      labels:
        app: vllm-serving
    spec:
      containers:
        - name: vllm-container
          image: vllm/vllm-openai:v0.5.0
          args:
            - "--model"
            - "s3://ml-checkpoints-bucket/ray_results/llama3-deepspeed-finetune/checkpoint_000000"
            - "--tensor-parallel-size"
            - "2"
            - "--gpu-memory-utilization"
            - "0.90"
            - "--max-model-len"
            - "4096"
          ports:
            - containerPort: 8000
          resources:
            limits:
              cpu: "16"
              memory: "64Gi"
              nvidia.com/gpu: "2"
            requests:
              cpu: "8"
              memory: "32Gi"
              nvidia.com/gpu: "2"
          # Critical: Graceful shutdown to flush active token streams
          terminationGracePeriodSeconds: 60
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 120
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 120
            periodSeconds: 15

Cost Optimization & Day-2 Operations

Operating LLM infrastructure at scale requires strict governance over expensive GPU allocations.

1. Fault-Tolerant Preemptible/Spot Node Execution

High-end GPUs on cloud providers (e.g., AWS p4d.24xlarge or GCP g2-standard-96) offer savings of up to 70% on Spot/Preemptible markets. Ray natively accommodates node terminations.

  • Ensure TorchTrainer uses ray.train.RunConfig pointing to durable S3 storage.
  • Set max_failures in the Ray execution configuration to allow automatic worker replacement when a Spot instance is reclaimed:
run_config = ray.train.RunConfig(
    failure_config=ray.train.FailureConfig(max_failures=3),
    storage_path="s3://ml-checkpoints-bucket/ray_results",
)

2. Metrics & Telemetry with Prometheus + DCGM

Standard CPU/Memory metrics are insufficient for tracking ML hardware stability. Deploy the NVIDIA DCGM (Data Center GPU Manager) Exporter to expose hardware metrics to Prometheus.

Key alerts to set in Prometheus:

  • GPU Memory Utilization Leak: DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_FREE > 0.95 sustained during non-training phases.
  • Thermal Throttling: DCGM_FI_DEV_THERMAL_THROTTLE == 1 indicates hardware cooling degradation requiring pod rescheduling.
  • NVLink Errors: DCGM_FI_DEV_NVLINK_CRC_FLIT_ERROR_TOTAL > 0 signals physical link degradation across GPUs, which severely degrades DeepSpeed execution speeds.

Conclusion

By running Ray and DeepSpeed on top of Kubernetes, you build a private compute engine that rivals commercial SaaS providers in performance while maintaining absolute control over data privacy, infrastructure geometry, and compute efficiency.

Production Readiness Checklist

  • [ ] Mount /dev/shm as an RAM-backed emptyDir on all worker pods.
  • [ ] Tune NCCL environment variables for your cluster's network fabric.
  • [ ] Implement DeepSpeed ZeRO-3 to partition model states across multi-GPU nodes.
  • [ ] Store checkpoints directly to durable, external S3-compatible storage.
  • [ ] Deploy inference engines using vLLM with maxUnavailable: 0 for seamless rolling deployments.
  • [ ] Monitor real-time hardware performance metrics via NVIDIA DCGM Exporter.