Files
poimen-memory/tasks/M5.4-vllm-lora-serving.md
T

4.5 KiB

M5.4 — vLLM + --enable-lora

Field Value
Phase M5 — Post-training
Size L — 3+ days
Status Not started
Flags homelab
Spec inlined below
Blocks

Goal

A serving path that can load the memory adapter — because the current one cannot.

Facts (inlined — no spec read needed)

Ollama cannot hot-swap LoRA adapters. The controller runs on qwen2.5:3b-instruct served by Ollama today, which is fine for prompted-only use and a dead end for post-training. vLLM supports --enable-lora with --lora-modules name=path, serving base plus adapters from one resident model.

The pattern already exists in this cluster: the reasoning predictor is vllm/vllm-openai:v0.11.0 under KServe, adopted into ArgoCD as k8s/apps/llm-serving/reasoning.yaml. Copy its shape.

VRAM is the constraint that makes an adapter the right answer. One GPU, OLLAMA_MAX_LOADED_MODELS=2, currently holding ornith:35b + qwen2.5:3b. A separate full memory model evicts something, and eviction is a weights reload measured in tens of seconds — ornith's cold start already blew a 60s gateway timeout once. A LoRA rides on a resident base for near-zero extra VRAM.

Two hard-won operational facts to carry over:

  1. Kong reads timeouts from the Service, not the Ingress. konghq.com/read-timeout on an Ingress is ignored; it must reach the predictor Service, and KServe propagates InferenceService annotations there. Getting this wrong produces a 504 at exactly 60s on the first cold request.
  2. Readiness must mean "can serve", not "process is up." vLLM's startup probe needs a long failureThreshold — model load plus torch compile measured 108s
    • 55s on the 32B. Report ready too early and the first request 504s.

Steps

  1. k8s/apps/llm-serving/memory.yaml — InferenceService, vLLM, Qwen2.5-3B-Instruct.
  2. Args: --enable-lora, --max-lora-rank 32, --max-model-len 32768, --served-model-name memory.
  3. Adapter storage: a PVC or an initContainer fetching from object storage; --lora-modules memory-v1=/mnt/adapters/memory-v1.
  4. Kong timeout annotations on the InferenceService metadata so KServe propagates them to the Service.
  5. Startup probe with a generous failureThreshold, gated on the OpenAI /health endpoint.
  6. New Kong route /v1/memory/chat/completions in llm-routes.yaml, with the model-key-auth plugin — in namespace llm-serving, since a KongPlugin reference resolves in the annotated object's own namespace and a dangling one fails open.
  7. Commit, push, let ArgoCD sync. No kubectl apply.

Acceptance

  • Base model answers through /v1/memory/chat/completions.
  • A named adapter is selectable via the model field.
  • Unauthenticated requests to the new route return 401.
  • Cold start does not 504.

Verify

Harness: kubectl and curl against the live gateway after sync.

Integration testverify/m5.4.sh diffed against expected/m5.4.txt:

  1. a1_isvc_ready — InferenceService reports Ready.
  2. a2_base_completion — a completion naming the base model returns 200.
  3. a3_adapter_selectable — with a dummy adapter mounted, request model: memory-v1; assert 200 and that it differs from the base response.
  4. a4_auth_enforced — no key → 401; apikey header → 200.
  5. a5_cold_start_no_504 — delete the pod, wait for Ready, immediately send a request; assert 200, not 504. This is the regression test for the timeout bug.
  6. a6_timeouts_on_service — assert konghq.com/read-timeout is present on the predictor Service, not only the Ingress.
  7. a7_vram_headroom — with the memory model resident alongside the others, assert nvidia-smi free memory stays above a threshold.

Command: bash verify/m5.4.sh | diff - expected/m5.4.txt

False pass:

  • Testing after the model is warm. The 504 bug only appears on the first request after a restart, which is exactly the case assertion 5 forces.
  • Checking the timeout annotation on the Ingress. It is ignored there, and its presence is what makes the bug hard to see.

Traps

  • Adding a third resident model without checking VRAM. Something gets evicted, and the symptom is a slow unrelated model rather than an obvious failure.
  • Putting the KongPlugin in the wrong namespace. It fails open — the route serves unauthenticated and looks healthy, which is how the model routes ran with no auth for a while.

Background: DESIGN.md — Separate weights · k8s/apps/llm-serving/