Technology

A/B Testing ML Pod Scheduling

ab testing ml pod scheduling
A/B Testing ML Pod Scheduling | SiamCafe Blog
2025-11-21· อ. บอม — SiamCafe.net· 10,951 คำ

A/B Testing สำหรับ ML Models

A/B Testing เป็นวิธีมาตรฐานในการเปรียบเทียบ ML Models ใน Production แทนที่จะ Deploy Model ใหม่ทดแทนทันที ส่ง Traffic บางส่วนไป Model ใหม่ (Challenger) และส่วนที่เหลือไป Model เดิม (Champion) แล้ววัดผลด้วย Business Metrics จริง

สำหรับ Kubernetes ต้องจัดการทั้ง Traffic Splitting (แบ่ง Traffic), Pod Scheduling (วาง Pod บน Node ที่เหมาะสม โดยเฉพาะ GPU Nodes) และ Monitoring (เก็บ Metrics เปรียบเทียบ) บทความนี้แสดงวิธีทำทั้งหมดพร้อม Config จริง

Kubernetes Config สำหรับ A/B Testing

# === Istio VirtualService — Traffic Splitting ===
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: ml-model-ab-test
  namespace: ml-serving
spec:
  hosts:
    - ml-model.ml-serving.svc.cluster.local
  http:
    - match:
        - headers:
            x-model-version:
              exact: "challenger"
      route:
        - destination:
            host: ml-model-challenger
            port:
              number: 8080
    - route:
        - destination:
            host: ml-model-champion
            port:
              number: 8080
          weight: 90
        - destination:
            host: ml-model-challenger
            port:
              number: 8080
          weight: 10
---
# === Champion Model Deployment ===
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ml-model-champion
  namespace: ml-serving
  labels:
    app: ml-model
    version: champion
    model-version: v2.1
spec:
  replicas: 3
  selector:
    matchLabels:
      app: ml-model
      version: champion
  template:
    metadata:
      labels:
        app: ml-model
        version: champion
    spec:
      # Pod Scheduling — GPU Node Affinity
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: nvidia.com/gpu.product
                    operator: In
                    values: ["Tesla-T4", "A10G"]
        # Pod Anti-affinity — กระจาย Pod ไปคนละ Node
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              podAffinityTerm:
                labelSelector:
                  matchExpressions:
                    - key: app
                      operator: In
                      values: ["ml-model"]
                topologyKey: kubernetes.io/hostname
      # Tolerations สำหรับ GPU Nodes
      tolerations:
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
      # Topology Spread — กระจายข้าม AZ
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app: ml-model
      containers:
        - name: model-server
          image: registry/ml-model:v2.1-champion
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "2"
              memory: 4Gi
              nvidia.com/gpu: "1"
            limits:
              cpu: "4"
              memory: 8Gi
              nvidia.com/gpu: "1"
          env:
            - name: MODEL_VERSION
              value: "champion-v2.1"
            - name: AB_TEST_GROUP
              value: "control"
          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 60
            periodSeconds: 15
---
# === Challenger Model Deployment ===
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ml-model-challenger
  namespace: ml-serving
  labels:
    app: ml-model
    version: challenger
    model-version: v3.0-beta
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ml-model
      version: challenger
  template:
    metadata:
      labels:
        app: ml-model
        version: challenger
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: nvidia.com/gpu.product
                    operator: In
                    values: ["Tesla-T4", "A10G"]
      tolerations:
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
      containers:
        - name: model-server
          image: registry/ml-model:v3.0-challenger
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "2"
              memory: 4Gi
              nvidia.com/gpu: "1"
            limits:
              cpu: "4"
              memory: 8Gi
              nvidia.com/gpu: "1"
          env:
            - name: MODEL_VERSION
              value: "challenger-v3.0"
            - name: AB_TEST_GROUP
              value: "treatment"

Python — A/B Test Statistical Analysis

# ab_test_analyzer.py — วิเคราะห์ผล A/B Testing สำหรับ ML Models
import numpy as np
from scipy import stats
from dataclasses import dataclass
from typing import Optional
import json

@dataclass
class ABTestResult:
    test_name: str
    champion_metric: float
    challenger_metric: float
    p_value: float
    significant: bool
    winner: str
    confidence_interval: tuple
    sample_size_champion: int
    sample_size_challenger: int
    effect_size: float

class MLABTestAnalyzer:
    """วิเคราะห์ A/B Testing สำหรับ ML Models"""

    def __init__(self, alpha=0.05, power=0.8):
        self.alpha = alpha
        self.power = power

    def calculate_sample_size(self, baseline_rate, mde, alpha=None,
                              power=None):
        """คำนวณ Sample Size ที่ต้องการ"""
        alpha = alpha or self.alpha
        power = power or self.power

        z_alpha = stats.norm.ppf(1 - alpha / 2)
        z_power = stats.norm.ppf(power)
        p1 = baseline_rate
        p2 = baseline_rate + mde

        n = ((z_alpha * np.sqrt(2 * p1 * (1 - p1)) +
              z_power * np.sqrt(p1 * (1 - p1) + p2 * (1 - p2))) /
             mde) ** 2

        return int(np.ceil(n))

    def test_conversion(self, champion_conversions, champion_total,
                        challenger_conversions, challenger_total):
        """Chi-squared Test สำหรับ Conversion Rate"""
        champion_rate = champion_conversions / champion_total
        challenger_rate = challenger_conversions / challenger_total

        contingency = np.array([
            [champion_conversions, champion_total - champion_conversions],
            [challenger_conversions, challenger_total - challenger_conversions],
        ])

        chi2, p_value, _, _ = stats.chi2_contingency(contingency)

        # Effect Size (Cohen's h)
        h1 = 2 * np.arcsin(np.sqrt(champion_rate))
        h2 = 2 * np.arcsin(np.sqrt(challenger_rate))
        effect_size = abs(h2 - h1)

        # Confidence Interval for difference
        diff = challenger_rate - champion_rate
        se = np.sqrt(champion_rate * (1 - champion_rate) / champion_total +
                     challenger_rate * (1 - challenger_rate) / challenger_total)
        ci = (diff - 1.96 * se, diff + 1.96 * se)

        winner = "challenger" if challenger_rate > champion_rate and \
                 p_value < self.alpha else "champion"

        return ABTestResult(
            test_name="Conversion Rate",
            champion_metric=champion_rate,
            challenger_metric=challenger_rate,
            p_value=p_value,
            significant=p_value < self.alpha,
            winner=winner,
            confidence_interval=ci,
            sample_size_champion=champion_total,
            sample_size_challenger=challenger_total,
            effect_size=effect_size,
        )

    def test_continuous(self, champion_values, challenger_values,
                        metric_name="Latency"):
        """T-test สำหรับ Continuous Metrics (เช่น Latency)"""
        t_stat, p_value = stats.ttest_ind(champion_values,
                                           challenger_values)

        champion_mean = np.mean(champion_values)
        challenger_mean = np.mean(challenger_values)

        # Cohen's d
        pooled_std = np.sqrt(
            (np.std(champion_values)**2 + np.std(challenger_values)**2) / 2
        )
        effect_size = abs(champion_mean - challenger_mean) / pooled_std

        # CI for difference
        diff = challenger_mean - champion_mean
        se = np.sqrt(np.var(champion_values) / len(champion_values) +
                     np.var(challenger_values) / len(challenger_values))
        ci = (diff - 1.96 * se, diff + 1.96 * se)

        # สำหรับ Latency ต่ำกว่าดีกว่า
        if "latency" in metric_name.lower():
            winner = "challenger" if challenger_mean < champion_mean and \
                     p_value < self.alpha else "champion"
        else:
            winner = "challenger" if challenger_mean > champion_mean and \
                     p_value < self.alpha else "champion"

        return ABTestResult(
            test_name=metric_name,
            champion_metric=champion_mean,
            challenger_metric=challenger_mean,
            p_value=p_value,
            significant=p_value < self.alpha,
            winner=winner,
            confidence_interval=ci,
            sample_size_champion=len(champion_values),
            sample_size_challenger=len(challenger_values),
            effect_size=effect_size,
        )

    def print_report(self, results: list[ABTestResult]):
        """แสดงรายงาน A/B Test"""
        print("=" * 60)
        print("A/B Test Report — ML Model Comparison")
        print("=" * 60)

        for r in results:
            sig = "YES" if r.significant else "NO"
            print(f"\n--- {r.test_name} ---")
            print(f"  Champion:   {r.champion_metric:.4f} "
                  f"(n={r.sample_size_champion})")
            print(f"  Challenger: {r.challenger_metric:.4f} "
                  f"(n={r.sample_size_challenger})")
            print(f"  p-value:    {r.p_value:.4f}")
            print(f"  Significant: {sig} (alpha={self.alpha})")
            print(f"  Effect Size: {r.effect_size:.3f}")
            print(f"  95% CI:     ({r.confidence_interval[0]:.4f}, "
                  f"{r.confidence_interval[1]:.4f})")
            print(f"  Winner:     {r.winner.upper()}")

# ตัวอย่าง
analyzer = MLABTestAnalyzer(alpha=0.05)

# Sample Size Calculation
n = analyzer.calculate_sample_size(baseline_rate=0.05, mde=0.01)
print(f"Required sample size per group: {n}")

# Conversion Test
conv = analyzer.test_conversion(
    champion_conversions=500, champion_total=10000,
    challenger_conversions=550, challenger_total=10000,
)

# Latency Test
np.random.seed(42)
champ_latency = np.random.normal(45, 10, 5000)
chall_latency = np.random.normal(42, 9, 5000)
lat = analyzer.test_continuous(champ_latency, chall_latency, "Latency (ms)")

analyzer.print_report([conv, lat])

Monitoring A/B Test ด้วย Prometheus

# === Prometheus Queries สำหรับ A/B Test Monitoring ===

# 1. Request Rate per Model Version
rate(ml_model_requests_total{namespace="ml-serving"}[5m])

# 2. Latency P99 per Model Version
histogram_quantile(0.99,
  rate(ml_model_latency_seconds_bucket{namespace="ml-serving"}[5m])
)

# 3. Error Rate per Model Version
sum(rate(ml_model_errors_total{namespace="ml-serving"}[5m])) by (version)
/
sum(rate(ml_model_requests_total{namespace="ml-serving"}[5m])) by (version)

# 4. GPU Utilization per Pod
avg(DCGM_FI_DEV_GPU_UTIL{namespace="ml-serving"}) by (pod)

# === Grafana Dashboard JSON (Panel) ===
# {
#   "title": "A/B Test — Model Comparison",
#   "panels": [
#     {
#       "title": "Request Rate by Version",
#       "targets": [{"expr": "rate(ml_requests_total[5m])"}],
#       "type": "timeseries"
#     },
#     {
#       "title": "P99 Latency by Version",
#       "targets": [{"expr": "histogram_quantile(0.99, rate(ml_latency_bucket[5m]))"}],
#       "type": "timeseries"
#     }
#   ]
# }

# === kubectl Commands สำหรับ Pod Scheduling ===

# ดู Pod Placement
kubectl get pods -n ml-serving -o wide \
  -l app=ml-model

# ดู Node Resources (GPU)
kubectl describe nodes | grep -A5 "Allocated resources"

# ดู GPU Utilization
kubectl top pods -n ml-serving --containers

# Scale Challenger ขึ้นเมื่อ A/B Test ผ่าน
kubectl scale deployment ml-model-challenger \
  -n ml-serving --replicas=3

# อัปเดต Traffic Weight (Istio)
kubectl patch virtualservice ml-model-ab-test \
  -n ml-serving --type merge \
  -p '{"spec":{"http":[{"route":[
    {"destination":{"host":"ml-model-champion"},"weight":50},
    {"destination":{"host":"ml-model-challenger"},"weight":50}
  ]}]}}'

Best Practices

A/B Testing สำหรับ ML Models คืออะไร

เปรียบเทียบ Model 2 ตัวขึ้นไปโดยส่ง Traffic จริงตามสัดส่วน วัดผลด้วย Metrics จริง เช่น Accuracy Latency Conversion Rate ตัดสินใจว่า Model ไหนดีกว่าก่อน Deploy เต็ม 100%

Pod Scheduling ใน Kubernetes คืออะไร

กระบวนการที่ Scheduler ตัดสินใจวาง Pod บน Node ไหน พิจารณาจาก Resource Requests, Node Affinity, Taints/Tolerations, Topology Spread สำหรับ ML ต้องจัดการ GPU Scheduling ด้วย

วิธีแบ่ง Traffic สำหรับ A/B Testing ทำอย่างไร

ใช้ Istio VirtualService กำหนด Weight เช่น 90/10 หรือ Header-based Routing ใช้ Seldon Core หรือ KServe สำหรับ ML-specific A/B Testing ที่มี Built-in Traffic Splitting

วิธีวิเคราะห์ผล A/B Testing ทำอย่างไร

ใช้ Statistical Hypothesis Testing เช่น Chi-squared สำหรับ Conversion T-test สำหรับ Continuous Metrics กำหนด Sample Size ล่วงหน้า ตั้ง Significance Level p < 0.05 รอจนได้ Sample เพียงพอก่อนสรุป

สรุป

A/B Testing สำหรับ ML Models บน Kubernetes ต้องจัดการทั้ง Traffic Splitting ด้วย Istio, Pod Scheduling ด้วย Node Affinity และ GPU Tolerations, Statistical Analysis ด้วย Hypothesis Testing และ Monitoring ด้วย Prometheus เริ่ม Traffic น้อย คำนวณ Sample Size ก่อน อย่าสรุปผลเร็วเกินไป และมี Automated Rollback เมื่อ Challenger มีปัญหา

📖 บทความที่เกี่ยวข้อง

PHP Pest Testing Pod Schedulingอ่านบทความ → DNSSEC Implementation Pod Schedulingอ่านบทความ → eBPF Networking Pod Schedulingอ่านบทความ → Elixir Nerves IoT Pod Schedulingอ่านบทความ → Python FastAPI Pod Schedulingอ่านบทความ →

📚 ดูบทความทั้งหมด →