Published on

Day 43: Docker/K8s/GPU Serving Cho AI Workload

Authors

1. Mục tiêu bài học

Day 43 tập trung vào deployment layer cho AI system. Sau Day 40-42, bạn đã có RAG/LLM service, streaming API và các lựa chọn serving như managed LLM, vLLM hoặc TGI. Bài này trả lời câu hỏi thực tế hơn:

Làm sao đóng gói service đó thành artifact repeatable?
Làm sao reviewer chạy được bằng Docker Compose?
Nếu lên Kubernetes thì cần manifest nào?
Nếu workload cần GPU thì scheduler biết node nào có GPU bằng cách nào?
Nếu đưa vào production thì còn thiếu điều kiện gì?

Mục tiêu không phải học toàn bộ Kubernetes. Mục tiêu là biết đủ để deploy một AI workload có trách nhiệm:

  • Docker image nhỏ, reproducible, không chứa secret.
  • Runtime config đi qua environment variable hoặc secret store.
  • Health/readiness endpoint phản ánh đúng trạng thái model, vector DB và dependency.
  • Docker Compose chạy được local stack bằng một lệnh.
  • Kubernetes manifests có resource boundary, probes, rollout strategy và secret/config separation.
  • GPU pod chỉ chạy trên GPU node, request đúng nvidia.com/gpu và không chiếm GPU node cho workload thường.
  • Có trade-off rõ giữa Compose, Kubernetes, Helm, KServe, Ray Serve, managed LLM và self-host GPU.

2. Mental model

AI service production không chỉ là:

FastAPI app -> docker build -> docker run

Một deployment tốt phải quản lý cả artifact, runtime state và operational behavior:

Source code
  -> locked dependencies
  -> Docker image
  -> config/secrets
  -> runtime volumes
  -> health/readiness
  -> scheduler constraints
  -> rollout/rollback
  -> logs/metrics/traces
  -> smoke test

Với AI workload, bạn có thêm vài constraint không thường gặp ở backend CRUD:

  • Model loading có thể mất vài chục giây đến vài phút.
  • VRAM là tài nguyên giới hạn hơn CPU/RAM.
  • Context length và concurrency làm KV cache tăng nhanh.
  • Cold start có thể làm readiness sai nếu chỉ kiểm tra process alive.
  • GPU driver, CUDA runtime, PyTorch/TensorRT/vLLM version phải tương thích.
  • Vector DB và index version cần backup, migration và rollback plan.
  • Streaming response cần timeout, graceful shutdown và client disconnect handling.

3. Target architecture cho bài học

Phiên bản local bằng Docker Compose:

Browser / curl
  -> api: FastAPI RAG service
      -> qdrant: vector DB
      -> optional local model server
      -> optional managed LLM API
  -> volumes: document data, vector DB data, model cache

Phiên bản Kubernetes:

Ingress / Gateway
  -> Service api
      -> Deployment api
          -> ConfigMap: non-secret config
          -> Secret: API keys
          -> PVC: optional data/cache
      -> Service vector-db
      -> StatefulSet vector-db or managed vector DB
      -> Service model-server
      -> Deployment model-server on GPU nodes

Best practice trong nhiều team là tách API orchestration và model serving:

  • api: authentication, request validation, RAG orchestration, prompt policy, citations, tracing.
  • vector-db: Qdrant, pgvector, Milvus hoặc managed vector DB.
  • model-server: vLLM/TGI/Triton/Ollama hoặc managed LLM provider.
  • observability: logs, metrics, traces, dashboards, alerts.

Tách như vậy giúp scale API và GPU inference độc lập. API thường scale theo request count; GPU model server scale theo tokens/sec, queue depth, VRAM và p95 latency.

4. Docker image cho ML/AI

4.1 Nguyên tắc thiết kế image

Một Docker image dùng cho AI backend nên đạt các tiêu chí:

  • Pin base image và Python version.
  • Lock dependency bằng requirements.lock, uv.lock, poetry.lock hoặc equivalent.
  • Tách layer dependency và layer source code để tận dụng build cache.
  • Không copy .env, API key, dataset thô, vector index, model cache lớn hoặc notebook rác vào image.
  • Chạy bằng non-root user nếu service không cần privilege.
  • Log ra stdout/stderr.
  • HEALTHCHECK hoặc ít nhất app có /health/ready.
  • Dùng image tag immutable hoặc digest khi deploy production.
  • Không dùng latest trong production manifest.

.dockerignore quan trọng không kém Dockerfile:

.git
.venv
__pycache__
.pytest_cache
.mypy_cache
.ruff_cache
.env
.env.*
!.env.example
data/raw
data/uploads
data/vector_store
models
model_cache
reports
*.sqlite
*.db
*.parquet
*.pt
*.safetensors

Không ignore .env.example vì reviewer cần biết config nào phải khai báo.

4.2 Dockerfile CPU cho FastAPI RAG backend

Ví dụ này phù hợp với RAG API dùng managed LLM hoặc embedding API bên ngoài. Nó không cần GPU trong container.

# syntax=docker/dockerfile:1.7
FROM python:3.11-slim AS runtime

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    APP_HOME=/app

WORKDIR ${APP_HOME}

RUN groupadd --system app && useradd --system --gid app --home-dir ${APP_HOME} app

RUN apt-get update \
    && apt-get install -y --no-install-recommends curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.lock ./requirements.lock
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --upgrade pip \
    && pip install -r requirements.lock

COPY app ./app
COPY pyproject.toml README.md ./

RUN chown -R app:app ${APP_HOME}
USER app

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  CMD curl -fsS http://127.0.0.1:8000/health || exit 1

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]

Điểm gần production:

  • Có non-root user.
  • Có lock file thay vì dependency trôi nổi.
  • Cài dependency trước khi copy source code.
  • Có healthcheck.
  • Không copy toàn bộ repo một cách mù quáng.
  • Không bake secret vào image.

Nếu app cần package native như psycopg, pymupdf, torch, faiss, bạn có thể cần stage build riêng hoặc base image có đủ system library. Không chọn Alpine cho ML Python nếu dependency native làm build phức tạp; python:slim thường thực dụng hơn.

4.3 Dockerfile GPU cho local model/reranker service

GPU image chỉ cần khi container trực tiếp chạy model bằng CUDA. Nếu API chỉ gọi OpenAI/Anthropic/Azure hoặc gọi model server khác qua HTTP, API image không cần NVIDIA base image.

Ví dụ skeleton:

# syntax=docker/dockerfile:1.7
ARG CUDA_IMAGE=nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04
FROM ${CUDA_IMAGE} AS runtime

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    APP_HOME=/app \
    HF_HOME=/models/huggingface \
    TRANSFORMERS_CACHE=/models/huggingface

WORKDIR ${APP_HOME}

RUN apt-get update \
    && apt-get install -y --no-install-recommends python3 python3-pip curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

RUN groupadd --system app && useradd --system --gid app --home-dir ${APP_HOME} app

COPY requirements-gpu.lock ./requirements-gpu.lock
RUN --mount=type=cache,target=/root/.cache/pip \
    python3 -m pip install --upgrade pip \
    && python3 -m pip install -r requirements-gpu.lock

COPY app ./app
RUN mkdir -p /models/huggingface && chown -R app:app ${APP_HOME} /models
USER app

EXPOSE 8001

HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=5 \
  CMD curl -fsS http://127.0.0.1:8001/health || exit 1

CMD ["python3", "-m", "app.model_server"]

Lưu ý quan trọng:

  • CUDA image version phải tương thích với framework wheels bạn cài.
  • Host driver phải đủ mới cho CUDA runtime trong container.
  • Model cache nên mount volume/PVC, không copy model hàng chục GB vào image trừ khi bạn có lý do rõ.
  • start-period của healthcheck GPU thường dài hơn vì model load lâu.
  • Không chạy container GPU với --privileged chỉ để thấy GPU. Cài đúng NVIDIA runtime/toolkit.

5. .env.example

.env.example là contract giữa code và runtime. Nó phải đủ rõ để reviewer chạy được mà không đọc toàn bộ source.

# App
APP_ENV=local
APP_NAME=rag-serving
LOG_LEVEL=INFO
PORT=8000
WORKERS=1
REQUEST_TIMEOUT_SECONDS=60
MAX_UPLOAD_MB=25
MAX_CONCURRENT_REQUESTS=16

# LLM mode: managed | local
LLM_MODE=managed
LLM_PROVIDER=openai
LLM_MODEL=gpt-4.1-mini
OPENAI_API_KEY=replace-me

# Local model server, used when LLM_MODE=local
MODEL_SERVER_URL=http://model-server:8001/v1
MODEL_ID=meta-llama/Llama-3.1-8B-Instruct
MODEL_CACHE_DIR=/models/huggingface

# Embedding
EMBEDDING_PROVIDER=openai
EMBEDDING_MODEL=text-embedding-3-small
EMBEDDING_DIM=1536

# Vector DB
VECTOR_DB=qdrant
QDRANT_URL=http://qdrant:6333
QDRANT_COLLECTION=policy_chunks

# Index/prompt versioning
INDEX_VERSION=rag-v1
PROMPT_VERSION=answer-v1
CHUNKING_VERSION=chunk-v1

# Observability
OTEL_EXPORTER_OTLP_ENDPOINT=
TRACE_SAMPLE_RATE=1.0

Production note:

  • .env.example được commit.
  • .env không được commit.
  • Secret thật nên nằm trong secret manager, external secret controller hoặc CI/CD secret store.
  • Với Kubernetes, non-secret config vào ConfigMap, secret vào Secret hoặc external secret.

6. Docker Compose cho RAG stack

Compose dùng để local demo, integration test và portfolio review. Compose không thay thế Kubernetes production, nhưng nó là deliverable bắt buộc vì giúp người khác chạy hệ thống nhanh.

Ví dụ Compose gần production hơn bản tối giản:

name: rag-serving

services:
  api:
    build:
      context: ./backend
      dockerfile: Dockerfile
    image: rag-serving-api:${APP_IMAGE_TAG:-local}
    env_file:
      - .env
    environment:
      QDRANT_URL: http://qdrant:6333
    ports:
      - "8000:8000"
    depends_on:
      qdrant:
        condition: service_started
    volumes:
      - ./data/sample_docs:/app/data/sample_docs:ro
      - ./reports:/app/reports
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    restart: unless-stopped

  qdrant:
    image: qdrant/qdrant:${QDRANT_TAG:-v1.14.1}
    ports:
      - "6333:6333"
    volumes:
      - qdrant_data:/qdrant/storage
    restart: unless-stopped

  model-server:
    profiles: ["gpu"]
    build:
      context: ./model-server
      dockerfile: Dockerfile.gpu
    image: rag-model-server:${MODEL_IMAGE_TAG:-local}
    env_file:
      - .env
    ports:
      - "8001:8001"
    volumes:
      - model_cache:/models/huggingface
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: ["gpu"]
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8001/health"]
      interval: 30s
      timeout: 5s
      retries: 10
      start_period: 120s
    restart: unless-stopped

volumes:
  qdrant_data:
  model_cache:

Qdrant có các endpoint /healthz, /livez, /readyz, nhưng Docker healthcheck chạy bên trong container. Nếu image không có curl, wget hoặc binary healthcheck riêng, healthcheck trong Compose sẽ giòn. Cách thực dụng cho lab là để API /ready retry và báo dependency chưa sẵn sàng; với Kubernetes, dùng httpGet probe trực tiếp từ kubelet.

Chạy CPU path:

cp .env.example .env
docker compose up --build
curl -fsS http://localhost:8000/health
curl -fsS http://localhost:8000/ready

Chạy GPU profile:

docker compose --profile gpu up --build

Trade-off của Compose:

  • Mạnh: dễ chạy local, dễ review, ít dependency.
  • Yếu: không đại diện đầy đủ cho scheduling, autoscaling, rollout, secret management, network policy.
  • Best use: portfolio, integration test, demo environment, single-VM internal tool nhỏ.

7. Healthcheck và readiness cho AI service

Tối thiểu nên có:

GET /health
  -> process còn sống
  -> event loop phản hồi
  -> không gọi dependency nặng

GET /ready
  -> app config hợp lệ
  -> vector DB reachable
  -> collection/index tồn tại
  -> model provider reachable hoặc local model loaded
  -> migration/index version đúng

Ví dụ FastAPI:

from fastapi import APIRouter, status
from fastapi.responses import JSONResponse

router = APIRouter()

@router.get("/health")
async def health() -> dict[str, str]:
    return {"status": "ok"}

@router.get("/ready")
async def ready() -> JSONResponse:
    checks = {
        "config": True,
        "vector_db": await check_qdrant(),
        "llm": await check_llm_provider(),
        "index_version": await check_index_version(),
    }
    http_status = status.HTTP_200_OK if all(checks.values()) else status.HTTP_503_SERVICE_UNAVAILABLE
    return JSONResponse({"status": "ready" if http_status == 200 else "not_ready", "checks": checks}, status_code=http_status)

Không nên để /ready gọi một LLM generation đầy đủ cho mỗi probe; chi phí và latency sẽ tệ. Với managed LLM, check nhẹ bằng config/token validation hoặc endpoint metadata nếu provider hỗ trợ. Với local model, check trạng thái model loaded hoặc warmup marker nội bộ.

8. NVIDIA container stack

Để container dùng được NVIDIA GPU, cần phân biệt bốn lớp:

LớpVai trò
Host NVIDIA driverDriver thật trên node/VM, giao tiếp với GPU
NVIDIA Container ToolkitCho container runtime expose GPU device/library vào container
Container runtimeDocker/containerd/CRI-O được cấu hình runtime NVIDIA khi cần
Kubernetes device pluginAdvertise GPU thành schedulable resource như nvidia.com/gpu

Local Docker smoke test:

docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi

Kubernetes GPU node checklist:

nvidia-smi
kubectl get nodes
kubectl describe node <gpu-node> | grep -A5 "nvidia.com/gpu"
kubectl get pods -n nvidia-device-plugin

Theo Kubernetes, GPU được expose qua device plugin như custom schedulable resource. Với NVIDIA, resource phổ biến là:

resources:
  limits:
    nvidia.com/gpu: 1

Điểm dễ sai:

  • GPU phải nằm trong limits. Nếu khai báo cả requestslimits thì hai giá trị phải bằng nhau.
  • Pod không request nvidia.com/gpu thì scheduler không reserve GPU cho pod.
  • Taint chỉ chặn pod không phù hợp; nodeSelector/node affinity mới kéo pod về đúng GPU node.
  • Nếu GPU node bị taint, nvidia-device-plugin DaemonSet cũng cần toleration phù hợp để chạy trên node đó.
  • Không assume một GPU request luôn là độc quyền nếu cluster bật MIG, MPS hoặc time-slicing. Cần hiểu policy của cluster.

9. Kubernetes scheduling cho GPU

9.1 Label GPU node

Ví dụ label node có NVIDIA L4:

kubectl label node gpu-node-1 accelerator=nvidia-l4
kubectl label node gpu-node-1 workload=ai-inference

9.2 Taint GPU node

Mục tiêu là tránh workload thường chạy lên GPU node:

kubectl taint node gpu-node-1 nvidia.com/gpu=true:NoSchedule

9.3 Pod cần cả selector và toleration

nodeSelector:
  accelerator: nvidia-l4
  workload: ai-inference
tolerations:
  - key: nvidia.com/gpu
    operator: Exists
    effect: NoSchedule
containers:
  - name: model-server
    resources:
      limits:
        nvidia.com/gpu: 1

Ý nghĩa:

  • nodeSelector: chỉ chọn node có label phù hợp.
  • tolerations: cho phép pod chạy trên node có taint tương ứng.
  • nvidia.com/gpu: scheduler reserve GPU cho container.

Nếu chỉ có toleration mà không có selector, pod "được phép" chạy trên GPU node nhưng không bị bắt buộc chạy ở đó. Nếu chỉ có selector mà node có taint, pod vẫn không schedule được.

10. Kubernetes manifests gần production

10.1 ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: rag-api-config
data:
  APP_ENV: "production"
  LOG_LEVEL: "INFO"
  LLM_MODE: "managed"
  LLM_PROVIDER: "openai"
  LLM_MODEL: "gpt-4.1-mini"
  EMBEDDING_MODEL: "text-embedding-3-small"
  VECTOR_DB: "qdrant"
  QDRANT_URL: "http://qdrant:6333"
  QDRANT_COLLECTION: "policy_chunks"
  INDEX_VERSION: "rag-v1"
  PROMPT_VERSION: "answer-v1"
  REQUEST_TIMEOUT_SECONDS: "60"
  MAX_CONCURRENT_REQUESTS: "32"

10.2 Secret

apiVersion: v1
kind: Secret
metadata:
  name: rag-api-secret
type: Opaque
stringData:
  OPENAI_API_KEY: "replace-in-secret-manager"

Production không nên commit secret manifest chứa giá trị thật. Dùng External Secrets, Sealed Secrets, SOPS hoặc secret manager của cloud provider.

10.3 Deployment API

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rag-api
  labels:
    app: rag-api
spec:
  replicas: 2
  revisionHistoryLimit: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app: rag-api
  template:
    metadata:
      labels:
        app: rag-api
    spec:
      terminationGracePeriodSeconds: 60
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: api
          image: ghcr.io/acme/rag-api:2026.05.10-abcdef
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8000
              name: http
          envFrom:
            - configMapRef:
                name: rag-api-config
            - secretRef:
                name: rag-api-secret
          resources:
            requests:
              cpu: "500m"
              memory: "1Gi"
            limits:
              cpu: "2"
              memory: "4Gi"
          readinessProbe:
            httpGet:
              path: /ready
              port: http
            initialDelaySeconds: 10
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 6
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 30
            periodSeconds: 30
            timeoutSeconds: 3
            failureThreshold: 3
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]

10.4 Service

apiVersion: v1
kind: Service
metadata:
  name: rag-api
spec:
  type: ClusterIP
  selector:
    app: rag-api
  ports:
    - name: http
      port: 8000
      targetPort: http

10.5 GPU model server Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: model-server
  labels:
    app: model-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: model-server
  template:
    metadata:
      labels:
        app: model-server
    spec:
      terminationGracePeriodSeconds: 120
      nodeSelector:
        accelerator: nvidia-l4
        workload: ai-inference
      tolerations:
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
      containers:
        - name: model-server
          image: ghcr.io/acme/rag-model-server:2026.05.10-abcdef
          ports:
            - containerPort: 8001
              name: http
          env:
            - name: MODEL_ID
              value: "meta-llama/Llama-3.1-8B-Instruct"
            - name: MODEL_CACHE_DIR
              value: "/models/huggingface"
          resources:
            requests:
              cpu: "2"
              memory: "12Gi"
            limits:
              cpu: "8"
              memory: "24Gi"
              nvidia.com/gpu: 1
          volumeMounts:
            - name: model-cache
              mountPath: /models/huggingface
          readinessProbe:
            httpGet:
              path: /ready
              port: http
            initialDelaySeconds: 120
            periodSeconds: 15
            timeoutSeconds: 5
            failureThreshold: 20
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 180
            periodSeconds: 30
            timeoutSeconds: 5
            failureThreshold: 5
      volumes:
        - name: model-cache
          persistentVolumeClaim:
            claimName: model-cache-pvc

GPU production note:

  • replicas: 1 là mặc định an toàn cho một model server khi bạn chưa có load test.
  • Scale GPU theo queue depth, tokens/sec, p95 latency và VRAM, không chỉ CPU.
  • Nếu model load lâu, dùng readiness delay dài, rolling strategy cẩn thận và warmup trước khi nhận traffic.
  • Với multi-GPU hoặc tensor parallel, manifest cần thêm logic riêng của serving engine.

11. Helm overview

Helm là package manager cho Kubernetes. Với AI workload, Helm hữu ích khi bạn có nhiều manifest lặp lại theo environment:

charts/rag-serving/
  Chart.yaml
  values.yaml
  templates/
    deployment.yaml
    service.yaml
    configmap.yaml
    secret.yaml
    hpa.yaml

Khi nên dùng Helm:

  • Có nhiều environment: dev, staging, prod.
  • Cần parameterize image tag, replica, resource, node selector, secret reference.
  • Team đã có GitOps/Helm workflow.
  • Muốn reuse chart cho nhiều model/service.

Khi chưa nên dùng Helm:

  • Bạn mới học Kubernetes và manifest còn ít.
  • Team chưa có release workflow rõ.
  • Chart chỉ bọc lại 2-3 file YAML nhưng làm debug khó hơn.

Best path cho bài học:

  1. Viết raw manifests trước để hiểu object.
  2. Sau khi manifest ổn, mới chuyển sang Helm chart.
  3. Không dùng Helm để che lấp việc chưa hiểu scheduling, probes và resource.

12. KServe overview

KServe là một inference platform trên Kubernetes cho predictive và generative inference. Nó cung cấp abstraction như InferenceService, model runtime, autoscaling, protocol chuẩn và integration với model serving runtimes.

Khi KServe phù hợp:

  • Platform team đã vận hành Kubernetes tốt.
  • Có nhiều model cần deploy theo pattern giống nhau.
  • Cần standardized inference endpoint, autoscaling, canary/rollout, model runtime.
  • Muốn tách model serving platform khỏi business API.

Khi KServe có thể quá nặng:

  • Chỉ có một RAG app nhỏ.
  • Team chưa vận hành Kubernetes/GPU ổn định.
  • Logic chính nằm ở orchestration/RAG pipeline hơn là pure model inference.

Mental model:

Client/API
  -> InferenceService
      -> predictor runtime
      -> model storage/cache
      -> autoscaling
      -> standardized inference protocol

Với RAG, thường không đưa toàn bộ RAG orchestration vào KServe. Cách sạch hơn:

  • FastAPI vẫn làm RAG orchestration.
  • KServe phục vụ embedding/reranker/local LLM như model endpoint.
  • API gọi KServe endpoint qua HTTP/gRPC.

13. Ray Serve overview

Ray Serve là framework serving online inference chạy trên Ray. Nó hợp với model composition và pipeline nhiều bước bằng Python:

HTTP request
  -> Ray Serve deployment A: preprocess
  -> deployment B: retriever/reranker
  -> deployment C: model inference
  -> deployment D: postprocess

Khi Ray Serve phù hợp:

  • Pipeline inference nhiều model, nhiều bước, cần composition rõ.
  • Cần dynamic batching, streaming, multi-node hoặc multi-GPU scheduling.
  • Team đã dùng Ray cho batch/distributed workload.
  • Muốn scale từng deployment trong pipeline độc lập.

Khi Ray Serve có thể quá nặng:

  • RAG API đơn giản, traffic thấp.
  • Team chưa có kinh nghiệm vận hành Ray cluster.
  • Managed LLM đã đáp ứng latency/cost.

Rule thực dụng:

  • FastAPI + managed LLM: tốt cho MVP và nhiều production app vừa/nhỏ.
  • FastAPI + vLLM/TGI trên GPU: tốt khi cần self-host LLM rõ ràng.
  • KServe: tốt khi platform team chuẩn hóa model serving trên Kubernetes.
  • Ray Serve: tốt khi inference graph phức tạp và cần scale nhiều bước bằng Ray.

14. Trade-off matrix

Lựa chọnĐiểm mạnhĐiểm yếuKhi chọn
Docker ComposeChạy local nhanh, dễ reviewKhông có scheduling/rollout thậtPortfolio, demo, integration test
Raw K8s manifestsRõ object, ít magicLặp YAML, khó quản lý nhiều envHọc, service ít, platform nhỏ
HelmTemplate hóa releaseDebug chart/values phức tạpNhiều env/service, GitOps
Managed LLMÍt ops GPU, ship nhanhCost, privacy, rate limit, vendor dependencyMVP, business app, traffic vừa
Self-host GPUControl model/data/latencyCần GPU ops, capacity planningPrivacy cao, volume lớn, custom model
KServeChuẩn hóa model servingPlatform overheadNhiều model trên K8s
Ray ServeComposition, batching, distributedVận hành Ray clusterMulti-model pipeline phức tạp
Image chứa modelStartup nhanh hơnImage rất lớn, rollback chậmEdge/offline, model nhỏ, release ít
Mount model cacheImage nhỏ, update dễCold start tải modelDev/staging, model lớn, cache tốt
Scale API replicasRẻ và dễDependency/shared state cần thiết kếAPI stateless
Scale GPU replicasTăng throughputTốn GPU, warmup lâuTraffic cao, queue depth cao

15. Best solution theo context/performance

Không có một deployment strategy tốt nhất cho mọi AI workload. Dưới đây là lựa chọn thực dụng.

ContextBest solutionLý do
Capstone/portfolioDocker Compose + CPU API + Qdrant + managed LLMReviewer chạy nhanh, ít yêu cầu phần cứng
Internal demo trên một VMDocker Compose hoặc systemd + Docker, managed LLMĐơn giản, đủ kiểm soát, chi phí ops thấp
Production nhỏ, traffic vừaKubernetes API stateless + managed vector DB + managed LLMTập trung vào reliability/security hơn GPU ops
Privacy cao, không gửi dữ liệu ra ngoàiKubernetes + self-host embedding/reranker/LLM trên GPUControl data egress, cần benchmark và hardening
Throughput LLM caoDedicated model server với vLLM/TGI + queue/batching + GPU autoscaling policyTối ưu tokens/sec và VRAM
Nhiều model/team cùng deployKServe + Helm/GitOps + shared observabilityChuẩn hóa platform, giảm drift
Inference pipeline nhiều bướcRay Serve hoặc service composition rõScale từng stage, batching và routing tốt hơn

Với bài Day 43, best baseline là:

Local/portfolio:
  Docker Compose
  FastAPI API
  Qdrant
  managed LLM by default
  optional GPU model-server profile

Kubernetes optional:
  Deployment + Service + ConfigMap + Secret
  resource requests/limits
  readiness/liveness probes
  GPU model-server manifest with nodeSelector/tolerations/nvidia.com/gpu

16. Production readiness answer

Dùng được trong production không?

Có, nhưng không phải chỉ với file Dockerfile và Compose của bài học. Bộ cấu hình trong bài này là production-oriented baseline, chưa phải production hoàn chỉnh cho mọi doanh nghiệp.

Nếu có thì cần điều kiện gì?

Cần tối thiểu các điều kiện sau:

  • Image được build trong CI, dependency locked, vulnerability scan, tag immutable hoặc digest.
  • Secret không nằm trong image, Git repo hoặc plain Kubernetes Secret không mã hóa ở rest ngoài chuẩn cluster.
  • /health/ready phản ánh đúng trạng thái app, vector DB, model provider và index version.
  • API stateless hoặc state được đưa vào DB/object storage/vector DB có backup.
  • Resource requests/limits được sizing bằng benchmark thật.
  • GPU node có driver, NVIDIA Container Toolkit, device plugin và monitoring đúng.
  • Rollout có smoke test, rollback plan cho image, prompt version, index version và model version.
  • Observability có structured logs, metrics p50/p95/p99 latency, error rate, token usage, cost và trace ID.
  • Security có authn/authz, tenant/ACL filter server-side, network policy, request size limit và rate limit.
  • Data có backup/restore test cho vector DB, metadata DB và document store.
  • Load test chứng minh p95 latency, throughput, VRAM và cost nằm trong SLO.
  • Có incident runbook: provider outage, vector DB down, GPU OOM, model server cold start, bad index release.

Nếu thiếu các điều kiện trên, hệ thống vẫn có thể dùng cho demo, staging hoặc internal prototype, nhưng chưa nên gọi là production-ready.

17. Checklist cuối bài

  • Dockerfile cho API.
  • .dockerignore.
  • .env.example đầy đủ.
  • docker-compose.yml chạy được CPU path.
  • Có optional GPU service/profile nếu dùng local model.
  • /health/ready.
  • Có smoke test script.
  • Có K8s Deployment, Service, ConfigMap, Secret.
  • Có GPU manifest dùng nodeSelector, tolerations và nvidia.com/gpu.
  • Có resource estimate CPU/RAM/VRAM.
  • Có trade-off và best solution theo context.
  • README/deployment note trả lời production readiness.

18. Nguồn tham khảo chính thức


Tài liệu

Tài liệu này gom các template thực dụng để bạn dùng khi làm lab Day 43 hoặc nâng cấp capstone Day 40. Hãy xem đây là starting point, không phải cấu hình production universal.

1. Project structure đề xuất

rag-serving/
  backend/
    app/
      main.py
      settings.py
      health.py
      rag_pipeline.py
    tests/
    Dockerfile
    requirements.in
    requirements.lock
    pyproject.toml
  model-server/
    app/
      model_server.py
    Dockerfile.gpu
    requirements-gpu.lock
  k8s/
    namespace.yaml
    configmap.yaml
    secret.example.yaml
    api-deployment.yaml
    api-service.yaml
    qdrant-statefulset.yaml
    qdrant-service.yaml
    model-server-deployment.yaml
    model-server-service.yaml
    pdb.yaml
  scripts/
    smoke-test.sh
    benchmark.sh
  data/
    sample_docs/
  reports/
  .dockerignore
  .env.example
  docker-compose.yml
  README.md

Rule ownership:

  • Source code vào image.
  • Runtime config vào env/config map/secret.
  • Runtime data vào volume/PVC/object storage.
  • Model cache lớn vào volume/PVC, không bake vào image mặc định.

2. Dockerfile template cho API

# syntax=docker/dockerfile:1.7
FROM python:3.11-slim AS runtime

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    APP_HOME=/app

WORKDIR ${APP_HOME}

RUN groupadd --system app && useradd --system --gid app --home-dir ${APP_HOME} app

RUN apt-get update \
    && apt-get install -y --no-install-recommends curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.lock ./requirements.lock
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --upgrade pip \
    && pip install -r requirements.lock

COPY app ./app
COPY pyproject.toml README.md ./

RUN chown -R app:app ${APP_HOME}
USER app

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  CMD curl -fsS http://127.0.0.1:8000/health || exit 1

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]

3. Dockerfile.gpu template

# syntax=docker/dockerfile:1.7
ARG CUDA_IMAGE=nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04
FROM ${CUDA_IMAGE} AS runtime

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    APP_HOME=/app \
    HF_HOME=/models/huggingface \
    TRANSFORMERS_CACHE=/models/huggingface

WORKDIR ${APP_HOME}

RUN apt-get update \
    && apt-get install -y --no-install-recommends python3 python3-pip curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

RUN groupadd --system app && useradd --system --gid app --home-dir ${APP_HOME} app

COPY requirements-gpu.lock ./requirements-gpu.lock
RUN --mount=type=cache,target=/root/.cache/pip \
    python3 -m pip install --upgrade pip \
    && python3 -m pip install -r requirements-gpu.lock

COPY app ./app
RUN mkdir -p /models/huggingface && chown -R app:app ${APP_HOME} /models
USER app

EXPOSE 8001

HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=5 \
  CMD curl -fsS http://127.0.0.1:8001/health || exit 1

CMD ["python3", "-m", "app.model_server"]

4. .env.example template

APP_ENV=local
APP_NAME=rag-serving
LOG_LEVEL=INFO
PORT=8000
WORKERS=1
REQUEST_TIMEOUT_SECONDS=60
MAX_UPLOAD_MB=25
MAX_CONCURRENT_REQUESTS=16

LLM_MODE=managed
LLM_PROVIDER=openai
LLM_MODEL=gpt-4.1-mini
OPENAI_API_KEY=replace-me

MODEL_SERVER_URL=http://model-server:8001/v1
MODEL_ID=meta-llama/Llama-3.1-8B-Instruct
MODEL_CACHE_DIR=/models/huggingface

EMBEDDING_PROVIDER=openai
EMBEDDING_MODEL=text-embedding-3-small
EMBEDDING_DIM=1536

VECTOR_DB=qdrant
QDRANT_URL=http://qdrant:6333
QDRANT_COLLECTION=policy_chunks

INDEX_VERSION=rag-v1
PROMPT_VERSION=answer-v1
CHUNKING_VERSION=chunk-v1

OTEL_EXPORTER_OTLP_ENDPOINT=
TRACE_SAMPLE_RATE=1.0

APP_IMAGE_TAG=local
MODEL_IMAGE_TAG=local
QDRANT_TAG=v1.14.1

5. Docker Compose template

name: rag-serving

services:
  api:
    build:
      context: ./backend
      dockerfile: Dockerfile
    image: rag-serving-api:${APP_IMAGE_TAG:-local}
    env_file:
      - .env
    environment:
      QDRANT_URL: http://qdrant:6333
    ports:
      - "8000:8000"
    depends_on:
      qdrant:
        condition: service_started
    volumes:
      - ./data/sample_docs:/app/data/sample_docs:ro
      - ./reports:/app/reports
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    restart: unless-stopped

  qdrant:
    image: qdrant/qdrant:${QDRANT_TAG:-v1.14.1}
    ports:
      - "6333:6333"
    volumes:
      - qdrant_data:/qdrant/storage
    restart: unless-stopped

  model-server:
    profiles: ["gpu"]
    build:
      context: ./model-server
      dockerfile: Dockerfile.gpu
    image: rag-model-server:${MODEL_IMAGE_TAG:-local}
    env_file:
      - .env
    ports:
      - "8001:8001"
    volumes:
      - model_cache:/models/huggingface
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: ["gpu"]
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8001/health"]
      interval: 30s
      timeout: 5s
      retries: 10
      start_period: 120s
    restart: unless-stopped

volumes:
  qdrant_data:
  model_cache:

Qdrant expose /healthz, /livez/readyz, nhưng Compose healthcheck chạy bên trong container. Nếu image không có curl, wget hoặc healthcheck binary, đừng thêm healthcheck giòn; hãy để API /ready retry/report trạng thái Qdrant. Trong Kubernetes, httpGet probe không cần tool bên trong container.

6. Kubernetes namespace

apiVersion: v1
kind: Namespace
metadata:
  name: rag-serving
  labels:
    name: rag-serving

7. Kubernetes ConfigMap

apiVersion: v1
kind: ConfigMap
metadata:
  name: rag-api-config
  namespace: rag-serving
data:
  APP_ENV: "production"
  APP_NAME: "rag-serving"
  LOG_LEVEL: "INFO"
  PORT: "8000"
  WORKERS: "2"
  REQUEST_TIMEOUT_SECONDS: "60"
  MAX_UPLOAD_MB: "25"
  MAX_CONCURRENT_REQUESTS: "32"
  LLM_MODE: "managed"
  LLM_PROVIDER: "openai"
  LLM_MODEL: "gpt-4.1-mini"
  EMBEDDING_MODEL: "text-embedding-3-small"
  EMBEDDING_DIM: "1536"
  VECTOR_DB: "qdrant"
  QDRANT_URL: "http://qdrant.rag-serving.svc.cluster.local:6333"
  QDRANT_COLLECTION: "policy_chunks"
  INDEX_VERSION: "rag-v1"
  PROMPT_VERSION: "answer-v1"
  CHUNKING_VERSION: "chunk-v1"

8. Kubernetes Secret example

apiVersion: v1
kind: Secret
metadata:
  name: rag-api-secret
  namespace: rag-serving
type: Opaque
stringData:
  OPENAI_API_KEY: "replace-in-secret-manager"

Không dùng file trên với secret thật. Trong production, ưu tiên External Secrets, Sealed Secrets, SOPS hoặc secret manager của cloud provider.

9. API Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rag-api
  namespace: rag-serving
  labels:
    app: rag-api
spec:
  replicas: 2
  revisionHistoryLimit: 5
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  selector:
    matchLabels:
      app: rag-api
  template:
    metadata:
      labels:
        app: rag-api
    spec:
      terminationGracePeriodSeconds: 60
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      containers:
        - name: api
          image: ghcr.io/acme/rag-api:2026.05.10-abcdef
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 8000
          envFrom:
            - configMapRef:
                name: rag-api-config
            - secretRef:
                name: rag-api-secret
          resources:
            requests:
              cpu: "500m"
              memory: "1Gi"
            limits:
              cpu: "2"
              memory: "4Gi"
          readinessProbe:
            httpGet:
              path: /ready
              port: http
            initialDelaySeconds: 10
            periodSeconds: 10
            timeoutSeconds: 3
            failureThreshold: 6
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 30
            periodSeconds: 30
            timeoutSeconds: 3
            failureThreshold: 3
          lifecycle:
            preStop:
              exec:
                command: ["/bin/sh", "-c", "sleep 10"]

10. API Service

apiVersion: v1
kind: Service
metadata:
  name: rag-api
  namespace: rag-serving
spec:
  type: ClusterIP
  selector:
    app: rag-api
  ports:
    - name: http
      port: 8000
      targetPort: http

11. Qdrant StatefulSet cho lab

Production lớn nên cân nhắc Qdrant chart/operator/managed service và backup rõ ràng. Template dưới đây chỉ đủ cho lab hoặc namespace nhỏ.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: qdrant
  namespace: rag-serving
spec:
  serviceName: qdrant
  replicas: 1
  selector:
    matchLabels:
      app: qdrant
  template:
    metadata:
      labels:
        app: qdrant
    spec:
      containers:
        - name: qdrant
          image: qdrant/qdrant:v1.14.1
          ports:
            - name: http
              containerPort: 6333
            - name: grpc
              containerPort: 6334
          resources:
            requests:
              cpu: "500m"
              memory: "1Gi"
            limits:
              cpu: "2"
              memory: "4Gi"
          readinessProbe:
            httpGet:
              path: /readyz
              port: http
            initialDelaySeconds: 10
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /healthz
              port: http
            initialDelaySeconds: 30
            periodSeconds: 30
          volumeMounts:
            - name: qdrant-data
              mountPath: /qdrant/storage
  volumeClaimTemplates:
    - metadata:
        name: qdrant-data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 20Gi

12. Qdrant Service

apiVersion: v1
kind: Service
metadata:
  name: qdrant
  namespace: rag-serving
spec:
  type: ClusterIP
  selector:
    app: qdrant
  ports:
    - name: http
      port: 6333
      targetPort: http
    - name: grpc
      port: 6334
      targetPort: grpc

13. GPU model server PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: model-cache-pvc
  namespace: rag-serving
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 100Gi

14. GPU model server Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: model-server
  namespace: rag-serving
  labels:
    app: model-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: model-server
  template:
    metadata:
      labels:
        app: model-server
    spec:
      terminationGracePeriodSeconds: 120
      nodeSelector:
        accelerator: nvidia-l4
        workload: ai-inference
      tolerations:
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
      containers:
        - name: model-server
          image: ghcr.io/acme/rag-model-server:2026.05.10-abcdef
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 8001
          env:
            - name: MODEL_ID
              value: "meta-llama/Llama-3.1-8B-Instruct"
            - name: MODEL_CACHE_DIR
              value: "/models/huggingface"
          resources:
            requests:
              cpu: "2"
              memory: "12Gi"
            limits:
              cpu: "8"
              memory: "24Gi"
              nvidia.com/gpu: 1
          readinessProbe:
            httpGet:
              path: /ready
              port: http
            initialDelaySeconds: 120
            periodSeconds: 15
            timeoutSeconds: 5
            failureThreshold: 20
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 180
            periodSeconds: 30
            timeoutSeconds: 5
            failureThreshold: 5
          volumeMounts:
            - name: model-cache
              mountPath: /models/huggingface
      volumes:
        - name: model-cache
          persistentVolumeClaim:
            claimName: model-cache-pvc

15. GPU model server Service

apiVersion: v1
kind: Service
metadata:
  name: model-server
  namespace: rag-serving
spec:
  type: ClusterIP
  selector:
    app: model-server
  ports:
    - name: http
      port: 8001
      targetPort: http

16. PodDisruptionBudget cho API

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: rag-api-pdb
  namespace: rag-serving
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: rag-api

Với GPU model server chỉ có một replica, PDB không tạo high availability thật. HA cho model server cần nhiều GPU replica, traffic routing và capacity đủ lớn.

17. Cài NVIDIA device plugin bằng Helm

Checklist trước:

nvidia-smi
containerd --version
kubectl get nodes

Ví dụ cài plugin:

helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
helm repo update
helm upgrade -i nvdp nvdp/nvidia-device-plugin \
  --namespace nvidia-device-plugin \
  --create-namespace \
  --version 0.17.1

Kiểm chứng:

kubectl get pods -n nvidia-device-plugin
kubectl describe node <gpu-node> | grep -A5 "nvidia.com/gpu"

Nếu GPU node có taint riêng, đảm bảo DaemonSet của device plugin có toleration tương ứng. Nếu không, plugin không chạy trên GPU node và Kubernetes sẽ không thấy nvidia.com/gpu.

18. GPU node label/taint commands

kubectl label node gpu-node-1 accelerator=nvidia-l4
kubectl label node gpu-node-1 workload=ai-inference
kubectl taint node gpu-node-1 nvidia.com/gpu=true:NoSchedule

Undo khi lab xong:

kubectl label node gpu-node-1 accelerator-
kubectl label node gpu-node-1 workload-
kubectl taint node gpu-node-1 nvidia.com/gpu=true:NoSchedule-

19. Helm values skeleton cho RAG API

image:
  repository: ghcr.io/acme/rag-api
  tag: "2026.05.10-abcdef"
  pullPolicy: IfNotPresent

replicaCount: 2

config:
  APP_ENV: production
  LOG_LEVEL: INFO
  LLM_MODE: managed
  LLM_PROVIDER: openai
  LLM_MODEL: gpt-4.1-mini
  QDRANT_URL: http://qdrant.rag-serving.svc.cluster.local:6333
  QDRANT_COLLECTION: policy_chunks

secretRefs:
  enabled: true
  name: rag-api-secret

resources:
  requests:
    cpu: 500m
    memory: 1Gi
  limits:
    cpu: "2"
    memory: 4Gi

probes:
  readiness:
    path: /ready
    initialDelaySeconds: 10
  liveness:
    path: /health
    initialDelaySeconds: 30

nodeSelector: {}
tolerations: []
affinity: {}

20. KServe skeleton

KServe nên phục vụ model endpoint, không nhất thiết chứa toàn bộ RAG orchestration.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: embedding-model
  namespace: rag-serving
spec:
  predictor:
    model:
      modelFormat:
        name: sklearn
      storageUri: s3://example-bucket/models/embedding-model
      resources:
        requests:
          cpu: "1"
          memory: "2Gi"
        limits:
          cpu: "2"
          memory: "4Gi"

Template trên chỉ là skeleton để hiểu shape của InferenceService. Runtime/model format thật phụ thuộc stack bạn chọn: built-in runtime, custom runtime, Hugging Face runtime, vLLM runtime hoặc runtime nội bộ.

21. Ray Serve skeleton

from ray import serve
from starlette.requests import Request

@serve.deployment(num_replicas=2)
class Retriever:
    async def __call__(self, query: str) -> list[dict]:
        return await search_vector_db(query)

@serve.deployment(ray_actor_options={"num_gpus": 1})
class Reranker:
    def __init__(self) -> None:
        self.model = load_reranker()

    async def __call__(self, query: str, docs: list[dict]) -> list[dict]:
        return self.model.rerank(query, docs)

@serve.deployment
class RagApp:
    def __init__(self, retriever, reranker) -> None:
        self.retriever = retriever
        self.reranker = reranker

    async def __call__(self, request: Request) -> dict:
        body = await request.json()
        docs = await self.retriever.remote(body["question"])
        ranked = await self.reranker.remote(body["question"], docs)
        return {"contexts": ranked[:5]}

app = RagApp.bind(Retriever.bind(), Reranker.bind())

Ray Serve hữu ích khi bạn muốn scale Retriever, Reranker và model generation như các deployment riêng trong cùng inference graph.

22. Smoke test script

#!/usr/bin/env bash
set -euo pipefail

BASE_URL="${BASE_URL:-http://localhost:8000}"

curl -fsS "${BASE_URL}/health" | jq .
curl -fsS "${BASE_URL}/ready" | jq .

curl -fsS -X POST "${BASE_URL}/query" \
  -H "Content-Type: application/json" \
  -d '{
    "question": "Nhân viên full-time có bao nhiêu ngày nghỉ phép năm?",
    "tenant_id": "demo",
    "user_roles": ["employee"]
  }' | jq .

Nếu không muốn phụ thuộc jq, in raw response và kiểm tra status code.

23. Benchmark script skeleton

#!/usr/bin/env bash
set -euo pipefail

BASE_URL="${BASE_URL:-http://localhost:8000}"
N="${N:-20}"

for i in $(seq 1 "${N}"); do
  start_ns=$(date +%s%N)
  curl -fsS -o /tmp/rag-response.json -X POST "${BASE_URL}/query" \
    -H "Content-Type: application/json" \
    -d '{"question":"Chính sách remote work áp dụng thế nào?","tenant_id":"demo","user_roles":["employee"]}'
  end_ns=$(date +%s%N)
  ms=$(( (end_ns - start_ns) / 1000000 ))
  echo "query=${i} latency_ms=${ms}"
done

Metric cần ghi lại:

  • p50/p95/p99 latency.
  • tokens input/output.
  • LLM cost nếu dùng managed provider.
  • CPU/RAM của API.
  • Qdrant RAM/disk.
  • GPU memory/utilization nếu dùng local model.
  • Error rate, timeout rate, queue depth.

24. Deployment note template

# Deployment Note: RAG Serving

## Architecture
- API: FastAPI RAG orchestration.
- Vector DB: Qdrant.
- LLM: managed OpenAI by default, optional local model server.
- Storage: Qdrant PVC and document object storage.

## Runtime config
- ConfigMap: non-secret runtime config.
- Secret: provider API key.
- Index version: rag-v1.
- Prompt version: answer-v1.

## Local run
```bash
cp .env.example .env
docker compose up --build
scripts/smoke-test.sh
```

## Kubernetes run
```bash
kubectl apply -f k8s/namespace.yaml
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/secret.example.yaml
kubectl apply -f k8s/
```

## Resource estimate
| Component | CPU | RAM | GPU | Notes |
|---|---:|---:|---:|---|
| API | 500m-2 CPU | 1-4Gi | 0 | Depends on concurrency |
| Qdrant | 500m-2 CPU | 1-4Gi | 0 | Depends on corpus/vector dim |
| Model server | 2-8 CPU | 12-24Gi | 1 | Depends on model size/context |

## Rollback
- Roll back API image tag.
- Roll back prompt version.
- Roll back index version or restore vector DB snapshot.
- Disable local model server and switch to managed provider if GPU path fails.

## Production readiness
Answer: not production-ready until CI build/scanning, secret management, backup/restore, monitoring/alerts, load test and security review are complete.

25. Production checklist

Docker:

  • Base image pinned.
  • Dependencies locked.
  • .dockerignore excludes secret/data/cache/model files.
  • Image runs non-root.
  • Health endpoint exists.
  • CI builds and scans image.
  • Production uses immutable tag or digest.

Compose:

  • docker compose up --build works from clean checkout.
  • .env.example documents all required variables.
  • Volumes are explicit.
  • API healthcheck exists; vector DB readiness được kiểm tra qua API /ready, Kubernetes probe hoặc healthcheck chính thức của image.
  • CPU path works without GPU.
  • GPU path is optional profile.

Kubernetes:

  • ConfigMap and Secret are separated.
  • Requests/limits are set.
  • Readiness/liveness probes are meaningful.
  • RollingUpdate strategy is configured.
  • Termination grace period handles streaming requests.
  • Service is ClusterIP behind ingress/gateway.
  • PDB/HPA are considered where appropriate.

GPU:

  • Host driver installed.
  • NVIDIA Container Toolkit configured.
  • nvidia-device-plugin running.
  • kubectl describe node shows nvidia.com/gpu.
  • GPU nodes labeled.
  • GPU nodes tainted to avoid random workloads.
  • GPU pods have nodeSelector, tolerations and nvidia.com/gpu limit.
  • VRAM, GPU utilization and queue depth are monitored.

Security:

  • No API key in image or Git.
  • Authn/authz implemented.
  • Tenant/ACL filter enforced server-side.
  • Network policy considered.
  • Upload size and file type restricted.
  • Rate limiting and timeout configured.

Observability:

  • Structured logs with request ID/trace ID.
  • Latency metrics by stage: retrieve, rerank, generate.
  • Token and cost metrics.
  • Error/timeout metrics.
  • Dashboards and alerts.
  • Smoke test runs after deploy.

Data:

  • Vector DB backup/restore tested.
  • Metadata DB backup/restore tested.
  • Document storage lifecycle defined.
  • Index version and embedding model version recorded.
  • Rollback plan for bad index release.

26. Nguồn tham khảo chính thức


Bài tập

Mục tiêu

Bạn sẽ lấy RAG app từ Day 40 hoặc một FastAPI LLM service tương đương, rồi biến nó thành deployment package có thể review:

  • Docker image cho API.
  • .dockerignore.
  • .env.example.
  • Docker Compose chạy API + vector DB.
  • Health/readiness endpoint.
  • Smoke test.
  • Kubernetes manifests optional.
  • GPU model server manifest optional.
  • Deployment note trả lời production readiness.

Thời lượng đề xuất:

  • Bản tối thiểu: 90 phút.
  • Bản tốt cho portfolio: 0.5-1 ngày.
  • Bản gần production hơn: 1-2 ngày, thêm CI, scanning, monitoring và backup.

0. Acceptance criteria

Hoàn thành bài tập khi bạn có:

  • docker compose up --build chạy được CPU path.
  • GET /health trả 200.
  • GET /ready trả 200 sau khi vector DB sẵn sàng.
  • .env.example đủ để người khác tạo .env.
  • Docker image không chứa .env, raw data lớn hoặc model cache lớn.
  • Compose có volume cho Qdrant/vector DB.
  • Có smoke test query một câu hỏi mẫu.
  • Có K8s manifests: Deployment, Service, ConfigMap, Secret.
  • Có GPU manifest optional dùng nodeSelector, tolerations và nvidia.com/gpu.
  • Có trade-off và production readiness answer trong README/deployment note.

1. Chuẩn bị app target

Nếu dùng Day 40, chọn backend FastAPI có endpoint query. Nếu chưa có app, tạo skeleton:

backend/
  app/
    main.py
    health.py
    settings.py
  requirements.in
  requirements.lock

API tối thiểu:

GET /health
GET /ready
POST /query

/query có thể gọi RAG pipeline thật hoặc mock response nếu bạn chỉ đang tập trung vào deployment. Nếu mock, README phải ghi rõ phần nào là mock.

2. Viết settings bằng environment variables

Tạo config contract:

APP_ENV
LOG_LEVEL
LLM_MODE
LLM_PROVIDER
LLM_MODEL
OPENAI_API_KEY
MODEL_SERVER_URL
EMBEDDING_MODEL
QDRANT_URL
QDRANT_COLLECTION
INDEX_VERSION
PROMPT_VERSION
REQUEST_TIMEOUT_SECONDS
MAX_CONCURRENT_REQUESTS

Yêu cầu:

  • Không hard-code API key.
  • Không hard-code URL localhost trong code backend; dùng QDRANT_URL.
  • Validate config khi app startup.
  • Log config non-secret để debug.

3. Thêm health và readiness

/health kiểm tra process:

@router.get("/health")
async def health() -> dict[str, str]:
    return {"status": "ok"}

/ready kiểm tra dependency nhẹ:

@router.get("/ready")
async def ready() -> JSONResponse:
    checks = {
        "qdrant": await check_qdrant(),
        "index_version": await check_index_version(),
        "llm_provider": await check_llm_provider(),
    }
    ok = all(checks.values())
    return JSONResponse(
        {"status": "ready" if ok else "not_ready", "checks": checks},
        status_code=200 if ok else 503,
    )

Không gọi full generation trong readiness probe. Probe chạy liên tục; gọi LLM thật có thể tốn tiền và làm service bị rate limit.

4. Tạo .dockerignore

.git
.venv
__pycache__
.pytest_cache
.mypy_cache
.ruff_cache
.env
.env.*
!.env.example
data/raw
data/uploads
data/vector_store
models
model_cache
reports
*.sqlite
*.db
*.parquet
*.pt
*.safetensors

Kiểm tra lại:

docker build --no-cache -t rag-api:test ./backend

Nếu build context quá lớn, .dockerignore chưa đủ tốt.

5. Viết Dockerfile API

Tạo backend/Dockerfile:

# syntax=docker/dockerfile:1.7
FROM python:3.11-slim AS runtime

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_NO_CACHE_DIR=1 \
    APP_HOME=/app

WORKDIR ${APP_HOME}

RUN groupadd --system app && useradd --system --gid app --home-dir ${APP_HOME} app

RUN apt-get update \
    && apt-get install -y --no-install-recommends curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.lock ./requirements.lock
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --upgrade pip \
    && pip install -r requirements.lock

COPY app ./app
RUN chown -R app:app ${APP_HOME}
USER app

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  CMD curl -fsS http://127.0.0.1:8000/health || exit 1

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]

Build:

docker build -t rag-api:local ./backend

Run thử:

docker run --rm --env-file .env -p 8000:8000 rag-api:local
curl -fsS http://localhost:8000/health

6. Tạo .env.example

APP_ENV=local
APP_NAME=rag-serving
LOG_LEVEL=INFO
PORT=8000
WORKERS=1
REQUEST_TIMEOUT_SECONDS=60
MAX_UPLOAD_MB=25
MAX_CONCURRENT_REQUESTS=16

LLM_MODE=managed
LLM_PROVIDER=openai
LLM_MODEL=gpt-4.1-mini
OPENAI_API_KEY=replace-me

MODEL_SERVER_URL=http://model-server:8001/v1
MODEL_ID=meta-llama/Llama-3.1-8B-Instruct
MODEL_CACHE_DIR=/models/huggingface

EMBEDDING_PROVIDER=openai
EMBEDDING_MODEL=text-embedding-3-small
EMBEDDING_DIM=1536

VECTOR_DB=qdrant
QDRANT_URL=http://qdrant:6333
QDRANT_COLLECTION=policy_chunks

INDEX_VERSION=rag-v1
PROMPT_VERSION=answer-v1
CHUNKING_VERSION=chunk-v1

OTEL_EXPORTER_OTLP_ENDPOINT=
TRACE_SAMPLE_RATE=1.0

Tạo .env local:

cp .env.example .env

Không commit .env.

7. Viết Docker Compose

Tạo docker-compose.yml:

name: rag-serving

services:
  api:
    build:
      context: ./backend
      dockerfile: Dockerfile
    image: rag-serving-api:${APP_IMAGE_TAG:-local}
    env_file:
      - .env
    environment:
      QDRANT_URL: http://qdrant:6333
    ports:
      - "8000:8000"
    depends_on:
      qdrant:
        condition: service_started
    volumes:
      - ./data/sample_docs:/app/data/sample_docs:ro
      - ./reports:/app/reports
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s
    restart: unless-stopped

  qdrant:
    image: qdrant/qdrant:${QDRANT_TAG:-v1.14.1}
    ports:
      - "6333:6333"
    volumes:
      - qdrant_data:/qdrant/storage
    restart: unless-stopped

volumes:
  qdrant_data:

Lưu ý: Qdrant có endpoint /readyz, nhưng Compose healthcheck chạy bên trong container. Nếu image không có curl/wget, đừng thêm healthcheck giòn; thay vào đó /ready của API phải retry Qdrant và trả 503 cho đến khi dependency sẵn sàng.

Chạy:

docker compose up --build

Kiểm tra:

curl -fsS http://localhost:8000/health
curl -fsS http://localhost:8000/ready

8. Thêm optional GPU profile

Nếu bạn có local model server, thêm service:

  model-server:
    profiles: ["gpu"]
    build:
      context: ./model-server
      dockerfile: Dockerfile.gpu
    image: rag-model-server:${MODEL_IMAGE_TAG:-local}
    env_file:
      - .env
    ports:
      - "8001:8001"
    volumes:
      - model_cache:/models/huggingface
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: ["gpu"]
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8001/health"]
      interval: 30s
      timeout: 5s
      retries: 10
      start_period: 120s
    restart: unless-stopped

Thêm volume:

volumes:
  qdrant_data:
  model_cache:

Chạy:

docker compose --profile gpu up --build

Trước khi chạy GPU path, kiểm tra:

docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi

9. Viết smoke test

Tạo scripts/smoke-test.sh:

#!/usr/bin/env bash
set -euo pipefail

BASE_URL="${BASE_URL:-http://localhost:8000}"

curl -fsS "${BASE_URL}/health"
echo
curl -fsS "${BASE_URL}/ready"
echo

curl -fsS -X POST "${BASE_URL}/query" \
  -H "Content-Type: application/json" \
  -d '{
    "question": "Nhân viên full-time có bao nhiêu ngày nghỉ phép năm?",
    "tenant_id": "demo",
    "user_roles": ["employee"]
  }'
echo

Chạy:

chmod +x scripts/smoke-test.sh
scripts/smoke-test.sh

10. Viết Kubernetes manifests

Tạo các file trong k8s/.

configmap.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: rag-api-config
data:
  APP_ENV: "production"
  LOG_LEVEL: "INFO"
  LLM_MODE: "managed"
  LLM_PROVIDER: "openai"
  LLM_MODEL: "gpt-4.1-mini"
  EMBEDDING_MODEL: "text-embedding-3-small"
  QDRANT_URL: "http://qdrant:6333"
  QDRANT_COLLECTION: "policy_chunks"
  INDEX_VERSION: "rag-v1"
  PROMPT_VERSION: "answer-v1"

secret.example.yaml:

apiVersion: v1
kind: Secret
metadata:
  name: rag-api-secret
type: Opaque
stringData:
  OPENAI_API_KEY: "replace-in-secret-manager"

api-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rag-api
  labels:
    app: rag-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: rag-api
  template:
    metadata:
      labels:
        app: rag-api
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: api
          image: ghcr.io/your-org/rag-api:replace-with-tag
          ports:
            - name: http
              containerPort: 8000
          envFrom:
            - configMapRef:
                name: rag-api-config
            - secretRef:
                name: rag-api-secret
          resources:
            requests:
              cpu: "500m"
              memory: "1Gi"
            limits:
              cpu: "2"
              memory: "4Gi"
          readinessProbe:
            httpGet:
              path: /ready
              port: http
            initialDelaySeconds: 10
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 30
            periodSeconds: 30

api-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: rag-api
spec:
  type: ClusterIP
  selector:
    app: rag-api
  ports:
    - name: http
      port: 8000
      targetPort: http

Validate dry-run nếu có cluster context:

kubectl apply --dry-run=client -f k8s/

11. Optional GPU Kubernetes manifest

Chuẩn bị node:

kubectl label node gpu-node-1 accelerator=nvidia-l4
kubectl label node gpu-node-1 workload=ai-inference
kubectl taint node gpu-node-1 nvidia.com/gpu=true:NoSchedule

model-server-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: model-server
  labels:
    app: model-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: model-server
  template:
    metadata:
      labels:
        app: model-server
    spec:
      terminationGracePeriodSeconds: 120
      nodeSelector:
        accelerator: nvidia-l4
        workload: ai-inference
      tolerations:
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
      containers:
        - name: model-server
          image: ghcr.io/your-org/rag-model-server:replace-with-tag
          ports:
            - name: http
              containerPort: 8001
          env:
            - name: MODEL_ID
              value: "meta-llama/Llama-3.1-8B-Instruct"
          resources:
            requests:
              cpu: "2"
              memory: "12Gi"
            limits:
              cpu: "8"
              memory: "24Gi"
              nvidia.com/gpu: 1
          readinessProbe:
            httpGet:
              path: /ready
              port: http
            initialDelaySeconds: 120
            periodSeconds: 15
            failureThreshold: 20
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 180
            periodSeconds: 30

Kiểm tra GPU resource:

kubectl describe node gpu-node-1 | grep -A5 "nvidia.com/gpu"
kubectl describe pod <model-server-pod>

12. Benchmark

Chạy ít nhất 10 query:

for i in $(seq 1 10); do
  time curl -fsS -X POST http://localhost:8000/query \
    -H "Content-Type: application/json" \
    -d '{"question":"Chính sách remote work áp dụng thế nào?","tenant_id":"demo","user_roles":["employee"]}' \
    >/tmp/rag-response.json
done

Ghi vào reports/deployment-benchmark.md:

MetricKết quả
p50 latency
p95 latency
API CPU/RAM
Qdrant RAM/disk
GPU memoryoptional
Error rate
Token cost/querynếu dùng managed LLM

13. Trade-off phải ghi trong README

Trả lời các câu sau:

  1. Vì sao dùng managed LLM hay local model?
  2. Vì sao Compose đủ cho local nhưng chưa đủ cho production?
  3. Nếu deploy Kubernetes, API và model server scale khác nhau thế nào?
  4. Image có nên chứa model không, hay mount model cache?
  5. Vector DB tự vận hành hay dùng managed service?
  6. GPU node có cần taint không, vì sao?
  7. HPA theo CPU có đủ cho GPU inference không?

Gợi ý câu trả lời ngắn:

Với capstone, managed LLM là lựa chọn mặc định vì giảm GPU ops và giúp reviewer chạy được nhanh.
Local GPU model chỉ bật khi có yêu cầu privacy/cost/latency rõ và đã benchmark VRAM.
Compose dùng cho local review; production cần Kubernetes hoặc platform tương đương để có rollout, secret, scaling, network policy, observability và backup.

14. Production readiness answer

Trong README hoặc deployment note, trả lời trực tiếp:

## Dùng được trong production không?

Chưa, nếu chỉ dùng cấu hình lab. Có thể đưa vào production sau khi hoàn thành các điều kiện sau:

- CI build image, lock dependencies, scan vulnerability và tag immutable.
- Secret nằm trong secret manager, không nằm trong Git/image.
- Authn/authz và tenant/ACL filter chạy server-side.
- `/ready` kiểm tra vector DB, model provider và index version.
- Resource requests/limits được sizing bằng benchmark.
- Vector DB/document store có backup/restore test.
- Logs/metrics/traces có dashboard và alert.
- Có rate limit, timeout, upload limit và graceful shutdown.
- Có rollout/smoke test/rollback plan cho image, prompt, model và index.
- Nếu dùng GPU: node driver/toolkit/device plugin ổn định, có monitoring VRAM/GPU utilization và plan xử lý GPU OOM.

Nếu bạn muốn ghi "có thể dùng production", hãy ghi kèm phạm vi:

Có thể dùng production cho internal workload traffic thấp/trung bình nếu triển khai trên Kubernetes hoặc platform tương đương, dùng managed LLM/vector DB, bật auth/ACL, có backup, monitoring, rate limit và đã pass load test theo SLO.
Chưa phù hợp cho dữ liệu nhạy cảm hoặc traffic cao nếu chưa có security review, data governance và capacity planning.

15. Submission checklist

Nộp các artifact:

  • backend/Dockerfile.
  • .dockerignore.
  • .env.example.
  • docker-compose.yml.
  • scripts/smoke-test.sh.
  • k8s/configmap.yaml.
  • k8s/secret.example.yaml.
  • k8s/api-deployment.yaml.
  • k8s/api-service.yaml.
  • Optional k8s/model-server-deployment.yaml.
  • reports/deployment-benchmark.md.
  • README/deployment note có trade-off và production readiness answer.

16. Rubric

Hạng mụcĐiểmTiêu chí
Docker image20Build được, dependency locked, non-root, không leak secret/data
Compose20API + vector DB chạy được, healthcheck, volume, .env.example
Health/readiness15/health nhẹ, /ready kiểm tra dependency đúng
Kubernetes15Deployment/Service/ConfigMap/Secret, resource, probes
GPU awareness10Hiểu NVIDIA stack, có manifest nodeSelector/toleration/GPU limit
Smoke test/benchmark10Có script và báo cáo latency/resource
Trade-off/production readiness10Trả lời rõ dùng được production không và cần điều kiện gì

17. Lỗi thường gặp

  • Commit .env hoặc API key.
  • Docker image copy cả data/, .git, .venv, model cache.
  • Compose dùng localhost bên trong container để gọi service khác.
  • /ready luôn trả 200 dù vector DB chết.
  • Dùng latest cho production image.
  • Kubernetes manifest không có resource requests/limits.
  • GPU pod thiếu nvidia.com/gpu.
  • GPU pod có toleration nhưng không có nodeSelector, dẫn đến scheduling không như kỳ vọng.
  • GPU node không taint, workload thường chạy vào GPU node.
  • HPA chỉ nhìn CPU trong khi bottleneck thật là tokens/sec, queue depth hoặc VRAM.

18. Câu hỏi tự kiểm tra

  1. Docker image của bạn có chạy được nếu không mount source code không?
  2. Người khác có thể tạo .env chỉ từ .env.example không?
  3. Nếu Qdrant chưa ready, API có nhận traffic không?
  4. Nếu LLM provider timeout, request có timeout và log trace ID không?
  5. Nếu rolling deploy xảy ra trong lúc streaming, graceful shutdown xử lý thế nào?
  6. Nếu index version mới lỗi, rollback bằng cách nào?
  7. Nếu GPU OOM, bạn nhìn metric/log nào trước?
  8. Nếu traffic tăng 5 lần, bạn scale API, vector DB hay model server trước?