Navigation
๐Ÿญ Platform & Infrastructureยท 10 min read

CI/CD for Models

Automated evaluation, regression tests, format conversion, canary deployment โ€” the pipeline from checkpoint to production.

cicdautomationregressionconversiondeploymentpipeline

Model CI/CD automates the path from checkpoint to deployment. Unlike software CI/CD, models are multi-GB artifacts, evaluation takes GPU-hours, and format conversion (HuggingFace โ†’ TensorRT) is its own build step. But the principle is the same: every change passes automated quality gates before reaching production.


1. The Model CI/CD Pipeline

New checkpoint saved (or fine-tune completed)
  โ”‚
  โ”œโ”€โ†’ [Trigger] Webhook / filesystem watch / manual
  โ”‚
  โ”œโ”€โ†’ [Stage 1: Eval] Run benchmark suite on GPU cluster
  โ”‚     โ”œโ”€โ”€ MMLU, HumanEval, GSM8K (standard)
  โ”‚     โ”œโ”€โ”€ Custom domain benchmarks
  โ”‚     โ””โ”€โ”€ Safety evaluation (red-team prompts)
  โ”‚
  โ”œโ”€โ†’ [Gate 1: Quality] All metrics above threshold?
  โ”‚     โ”œโ”€โ”€ Yes โ†’ continue
  โ”‚     โ””โ”€โ”€ No โ†’ notify team, block pipeline
  โ”‚
  โ”œโ”€โ†’ [Stage 2: Regression] Compare against production model
  โ”‚     โ”œโ”€โ”€ Score delta within tolerance?
  โ”‚     โ””โ”€โ”€ Latency regression within tolerance?
  โ”‚
  โ”œโ”€โ†’ [Gate 2: Regression] No significant regressions?
  โ”‚     โ”œโ”€โ”€ Yes โ†’ continue
  โ”‚     โ””โ”€โ”€ No โ†’ notify team, block pipeline
  โ”‚
  โ”œโ”€โ†’ [Stage 3: Convert] Build serving formats
  โ”‚     โ”œโ”€โ”€ SafeTensors โ†’ AWQ INT4 quantization
  โ”‚     โ”œโ”€โ”€ SafeTensors โ†’ TensorRT-LLM engine
  โ”‚     โ””โ”€โ”€ SafeTensors โ†’ GGUF (if edge deployment)
  โ”‚
  โ”œโ”€โ†’ [Stage 4: Register] Add to model registry as "staging"
  โ”‚
  โ”œโ”€โ†’ [Stage 5: Canary] Deploy to 5% traffic
  โ”‚     โ””โ”€โ”€ Monitor 24h: latency, errors, quality
  โ”‚
  โ””โ”€โ†’ [Stage 6: Promote] Move to production (100% traffic)

2. Pipeline Implementation

GitHub Actions Example

name: Model CI/CD
on:
  workflow_dispatch:
    inputs:
      checkpoint_path:
        description: 'S3 path to model checkpoint'
        required: true
      model_name:
        description: 'Model name for registry'
        required: true

jobs:
  evaluate:
    runs-on: [self-hosted, gpu-h100]
    timeout-minutes: 120
    steps:
      - uses: actions/checkout@v4
      
      - name: Run evaluation suite
        run: |
          lm_eval --model vllm \
            --model_args pretrained=${{ inputs.checkpoint_path }},tensor_parallel_size=4 \
            --tasks mmlu,humaneval,gsm8k,ifeval \
            --batch_size auto \
            --output_path results/eval/
      
      - name: Run custom domain benchmarks
        run: python scripts/eval_custom.py --model ${{ inputs.checkpoint_path }}
      
      - name: Run safety evaluation
        run: python scripts/eval_safety.py --model ${{ inputs.checkpoint_path }}
      
      - name: Check quality gates
        run: |
          python scripts/check_gates.py \
            --results results/eval/ \
            --thresholds configs/quality_gates.yaml \
            --comparison-model production

  convert:
    needs: evaluate
    runs-on: [self-hosted, gpu-h100]
    steps:
      - name: Quantize to AWQ INT4
        run: |
          python scripts/quantize.py \
            --model ${{ inputs.checkpoint_path }} \
            --method awq \
            --bits 4 \
            --output s3://models/${{ inputs.model_name }}/staging/awq-int4/
      
      - name: Build TensorRT-LLM engine
        run: |
          trtllm-build \
            --checkpoint_dir ${{ inputs.checkpoint_path }} \
            --output_dir s3://models/${{ inputs.model_name }}/staging/trt/ \
            --gemm_plugin float16 \
            --max_batch_size 256

  register:
    needs: convert
    runs-on: ubuntu-latest
    steps:
      - name: Register in model registry
        run: |
          python scripts/register_model.py \
            --name ${{ inputs.model_name }} \
            --stage staging \
            --eval-results results/eval/ \
            --artifacts s3://models/${{ inputs.model_name }}/staging/

  canary:
    needs: register
    runs-on: ubuntu-latest
    steps:
      - name: Deploy canary (5% traffic)
        run: |
          kubectl apply -f deployments/canary.yaml
          python scripts/monitor_canary.py --duration 24h --threshold configs/canary_gates.yaml
      
      - name: Promote to production
        if: success()
        run: python scripts/promote.py --name ${{ inputs.model_name }} --stage production

3. Regression Tests

TestWhat it catchesThresholdMeasurement
MMLU dropGeneral knowledge regression<1% drop vs productionAbsolute score comparison
HumanEval dropCode quality regression<2% dropPass@1 comparison
GSM8K dropMath reasoning regression<2% dropAccuracy comparison
Latency regressionSlower inferenceP95 TPOT <1.05ร— productionBenchmark on same hardware
Safety regressionMore harmful outputsRefusal rate within 5% of baselineRed-team prompt set
Format complianceJSON/structured output breaksValid output rate <2% dropFormat validation tests
# Quality gate checker
def check_gates(new_results: dict, production_results: dict, thresholds: dict) -> list:
    """Check all quality gates, return list of failures."""
    failures = []
    
    for metric, threshold in thresholds.items():
        new_score = new_results.get(metric)
        prod_score = production_results.get(metric)
        
        if threshold["type"] == "absolute":
            if new_score < threshold["min"]:
                failures.append(f"{metric}: {new_score:.3f} < {threshold['min']}")
        
        elif threshold["type"] == "relative":
            max_drop = threshold["max_regression"]
            if prod_score and (prod_score - new_score) > max_drop:
                failures.append(
                    f"{metric}: dropped {prod_score - new_score:.3f} "
                    f"(max allowed: {max_drop})"
                )
    
    return failures

4. Format Conversion Pipeline

Each serving engine needs its own model format:

CONVERSION_MATRIX = {
    "vllm": {
        "input": "safetensors",
        "quantize": "awq_int4",       # or gptq, fp8
        "time": "30-60 min (70B)",
        "gpu_required": True,
    },
    "tensorrt_llm": {
        "input": "safetensors",
        "build": "trtllm-build",
        "time": "60-120 min (70B)",
        "gpu_required": True,         # Engine is GPU-specific
    },
    "llama_cpp": {
        "input": "safetensors",
        "convert": "convert_hf_to_gguf.py",
        "quantize": "q4_k_m",
        "time": "15-30 min (70B)",
        "gpu_required": False,        # CPU conversion
    },
}

Caching: Format conversion is deterministic โ€” cache converted artifacts by input hash. If the same checkpoint is re-evaluated, skip conversion.


5. Canary Monitoring

class CanaryMonitor:
    """Monitor canary deployment for 24 hours."""
    
    CANARY_GATES = {
        "error_rate": {"max": 0.001, "window": "1h"},
        "p99_ttft_ms": {"max_regression": 1.10, "window": "1h"},  # 10% slower OK
        "p99_tpot_ms": {"max_regression": 1.10, "window": "1h"},
        "quality_score": {"min": 3.5, "window": "4h"},  # LLM-as-judge sample
    }
    
    async def monitor(self, duration_hours: int = 24):
        start = time.time()
        while (time.time() - start) / 3600 < duration_hours:
            metrics = await self.collect_canary_metrics()
            
            for gate, threshold in self.CANARY_GATES.items():
                if not self.check(metrics, gate, threshold):
                    await self.rollback_canary()
                    raise CanaryFailed(f"Canary gate {gate} failed: {metrics[gate]}")
            
            await asyncio.sleep(300)  # Check every 5 minutes
        
        return CanaryResult(passed=True, duration=duration_hours)

6. Rollback

Every deployment must be instantly reversible:

async def rollback(model_name: str, reason: str):
    """Instant rollback to previous production model."""
    previous = await registry.get_previous_production(model_name)
    
    # Swap routing (model is already loaded on GPUs from before)
    await router.set_production_model(model_name, previous.version)
    
    # Update registry
    current = await registry.get_production(model_name)
    current.status = "failed"
    previous.status = "production"
    await registry.update(current)
    await registry.update(previous)
    
    # Alert team
    await alert(f"Rollback: {model_name} reverted to {previous.version}. Reason: {reason}")

Pre-condition: Keep the previous production model loaded (or quickly loadable) for at least 48 hours after a new deployment.

For the evaluation suite that powers CI/CD gates, see Offline Evaluation. For model registration, see Model Registry.


Self-Check

  • Design a 6-stage model CI/CD pipeline with quality gates
  • Implement regression tests comparing new model against production baseline
  • Set up automated format conversion (SafeTensors โ†’ AWQ โ†’ TensorRT)
  • Build a canary monitor with automatic rollback on gate failure
  • Ensure instant rollback capability for any production deployment

References