# Ollama LLM Inference Service CPU-only LLM inference server on talos-cp-1. Single model hot-loaded (DeepSeek-R1:70b), 42GB, 70Gi memory limit. ## Quick Start ### Access via port-forward ```bash kubectl -n llm port-forward svc/ollama 11434:11434 curl http://localhost:11434/api/tags ``` ### Debug pod (in-cluster) ```bash kubectl run debug --rm -it -n llm --image=curlimages/curl \ --labels="app.kubernetes.io/role=llm-debug" \ --serviceaccount=llm-worker -- sh # Inside pod TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) curl -H "Authorization: Bearer $TOKEN" \ http://ollama.llm.svc.cluster.local:11434/api/tags ``` ## Architecture | Component | Value | |-----------|-------| | Service | ClusterIP `ollama.llm.svc.cluster.local:11434` | | Namespace | `llm` | | Node | talos-cp-1 (pinned via nodeAffinity) | | Memory request | 50Gi | | Memory limit | 70Gi | | Storage | 115Gi PVC (Longhorn) | | Model | `deepseek-r1:70b` (~42GB) | | Max loaded | 1 model | | Parallelism | 1 request at a time | ## API Endpoints ### List models ```bash curl http://ollama.llm.svc.cluster.local:11434/api/tags ``` Response: ```json { "models": [ {"name": "deepseek-r1:70b", "size": 42000000000, ...} ] } ``` ### Generate (non-streaming) ```bash curl -X POST http://ollama.llm.svc.cluster.local:11434/api/generate \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-r1:70b", "prompt": "Why is the sky blue?", "stream": false }' ``` ### Pull model ```bash curl -X POST http://ollama.llm.svc.cluster.local:11434/api/pull \ -H "Content-Type: application/json" \ -d '{"name": "deepseek-r1:70b", "stream": false}' ``` ## Operations ### Check pod status ```bash kubectl -n llm get pod -l app.kubernetes.io/name=ollama kubectl -n llm describe pod -l app.kubernetes.io/name=ollama ``` ### View logs ```bash kubectl -n llm logs deployment/ollama -f ``` ### Monitor download progress (bootstrap) ```bash kubectl -n llm logs -f job/bootstrap-models -c model-download ``` ### Restart deployment ```bash kubectl -n llm rollout restart deployment/ollama ``` ## Storage - **PVC:** `ollama-models-cache`, 115Gi, Longhorn StorageClass - **Mount:** `/root/.ollama/models` (Ollama model cache) - **Lifecycle:** RWO (Read-Write-Once), tied to talos-cp-1 ### Resize PVC ⚠️ PVCs can only expand, not shrink. Edit values.yaml and redeploy: ```yaml pvc: size: 120Gi # increase only ``` ```bash vsource .env && helmfile -f helmfile.yaml.gotmpl -l name=ollama apply ``` ## Networking ### NetworkPolicy - Default-deny ingress on Ollama pods - Allow from pods labeled `app.kubernetes.io/name: llm-worker` (port 11434) - Allow from pods labeled `app.kubernetes.io/role: llm-debug` (port 11434) View policy: ```bash kubectl -n llm get networkpolicy ollama ``` Test access from external pod (should fail): ```bash kubectl run test --rm -it --image=curlimages/curl -- \ curl http://ollama.llm.svc.cluster.local:11434/ # Connection timeout (correct) ``` Test access from debug pod (should succeed): ```bash kubectl -n llm logs job/bootstrap-models # verify bootstrap completed # Then run debug pod as shown above ``` ## Configuration ### Helm values (`k8s/llm/charts/ollama/values.yaml`) ```yaml resources: requests: cpu: 8 memory: 50Gi limits: cpu: 16 memory: 70Gi env: OLLAMA_MAX_LOADED_MODELS: "1" OLLAMA_NUM_PARALLEL: "1" OLLAMA_MAX_QUEUE: "32" OLLAMA_KEEP_ALIVE: "-1" OLLAMA_HOST: "0.0.0.0:11434" preloadJob: enabled: true hotModels: - deepseek-r1:70b ``` ### Environment variables | Variable | Value | Purpose | |----------|-------|---------| | `OLLAMA_MODELS` | `/root/.ollama/models` | Model cache dir | | `OLLAMA_MAX_LOADED_MODELS` | `1` | Max concurrent models in RAM | | `OLLAMA_NUM_PARALLEL` | `1` | Parallel request threads | | `OLLAMA_MAX_QUEUE` | `32` | Request queue depth | | `OLLAMA_KEEP_ALIVE` | `-1` | Keep model resident (never unload) | | `OLLAMA_HOST` | `0.0.0.0:11434` | Bind address | Tune `OLLAMA_NUM_PARALLEL` based on CPU cores. Current: 1 (conservative, CPU bottleneck). ## Model Management ### Current model - **Name:** `deepseek-r1:70b` - **Size:** ~42GB - **Quantization:** Default Ollama quant - **Status:** Downloaded during pod init via bootstrap job ### Change model 1. Edit `values.yaml`: ```yaml preloadJob: hotModels: - deepseek-r1:32b # or any available model ``` 2. Redeploy: ```bash kubectl -n llm delete job bootstrap-models --ignore-not-found vsource .env && helmfile -f helmfile.yaml.gotmpl -l name=ollama apply ``` 3. Monitor: ```bash kubectl -n llm logs -f job/bootstrap-models -c model-download ``` ### Available models Ollama registry: https://ollama.com/library Examples: - `deepseek-r1:70b` (reasoning, 42GB) - `deepseek-r1:32b` (faster, 20GB) - `llama3.1:70b` (general, 41GB) - `mistral:large` (26GB) ## Troubleshooting ### Pod stuck in `ContainerCreating` ```bash kubectl -n llm describe pod -l app.kubernetes.io/name=ollama # Check Events section for PVC/image pull issues ``` ### Bootstrap job failing ```bash kubectl -n llm logs job/bootstrap-models -c model-download --tail=50 # Common: model not found in registry, disk full, network timeout ``` ### Model pull timeout ```bash # Increase pod timeout (edit deployment directly) kubectl -n llm edit deployment ollama # Change readinessProbe.initialDelaySeconds, livenessProbe.periodSeconds ``` ### Out of memory Model size exceeds limit. Reduce `memory.limits` or choose smaller model. ```bash kubectl top pod -n llm # check actual usage ``` ### Cannot connect from other pods Verify NetworkPolicy: ```bash kubectl -n llm get networkpolicy kubectl -n llm describe networkpolicy ollama # Add pod label: app.kubernetes.io/name: llm-worker or app.kubernetes.io/role: llm-debug ``` ## Secrets Ollama pod receives MinIO credentials via Secret `ollama-minio` (created by helmfile presync): ```bash kubectl -n llm get secret ollama-minio -o jsonpath='{.data}' | jq ``` Keys: `endpoint`, `bucket`, `access_key`, `secret_key` Used by bootstrap job to upload model blobs to MinIO (future: auto-backup). ## Metrics & Observability ### Prometheus scrape (if enabled) ServiceMonitor: Not yet configured (see `k8s/monitoring/dashboards/services/`) Metrics to add: - `ollama_requests_total` (counter) - `ollama_request_duration_seconds` (histogram) - `ollama_loaded_models` (gauge) ### Logs Pod logs via kubectl. No log aggregation to Loki yet. ```bash kubectl -n llm logs deployment/ollama -f --timestamps ``` ## Cleanup ### Delete Ollama completely ```bash vsource .env && helmfile -f helmfile.yaml.gotmpl -l name=ollama destroy # Keeps PVC (data safety). To delete: kubectl -n llm delete pvc ollama-models-cache ``` ### Delete just the model cache (keep deployment) ```bash kubectl -n llm delete pvc ollama-models-cache # Recreate: kubectl -n llm patch deployment ollama -p '{"spec":{"template":{"metadata":{"annotations":{"restart":"now"}}}}}' ``` ## See Also - Helmfile: `helmfile.yaml.gotmpl` (llm release block) - Chart: `k8s/llm/charts/ollama/` - Namespace: `llm` - Bootstrap: `k8s/llm/bootstrap-models-job.yaml` (manual preload fallback)