all_lessons/kubernetes_genai/16lesson 16 / 18

Part V - AI applications

RAG on Kubernetes

Lesson 15 taught you to share GPUs fairly between latency-sensitive inference and bulk batch work without one starving the other. RAG is the first application where that tension lives inside one product: a heavy write-time pipeline that reads, chunks, and embeds a corpus, and a thin read-time path that must answer a user in well under a second. The two pipelines are coupled by choices they must agree on — the same embedding model, the same chunking, the same index — yet they run as completely different Kubernetes workloads. This lesson treats RAG not as "a model plus a database" but as two systems that silently break each other when their assumptions drift apart.

Source coverage
PDF Chapter 8: retrieval-augmented generation components, ingestion, query processing, and RAG on Kubernetes.
Linear position
Prerequisite: Lesson 15 (Batch scheduling and GPU fairness) — you can run bulk GPU work as Jobs/CronJobs with quotas and priorities so it does not starve online serving, and you have the model-serving primitives from Part IV.
New capability: Deploy RAG as two coupled systems — an ingestion pipeline that produces trustworthy, versioned context, and a query path that consumes it under a latency budget — while keeping write-time and read-time choices consistent.
The plan
Five moves. (1) Define RAG and split it into a write path and a read path, and name the contract that binds them. (2) Walk the ingestion pipeline (load → chunk → embed → index → provenance) and why it belongs in Jobs/CronJobs, not the serving Deployment. (3) Walk the query pipeline (embed → search → rerank → assemble → generate with citations) under a latency budget. (4) Dramatize the central failure: an embedding-model mismatch between write and read that degrades retrieval silently, and the re-ingestion cost that makes an untested vector-DB restore catastrophic. (5) Failure modes, a checklist, and the hand-off into agents, where retrieval becomes one tool among many.

1 · RAG is two pipelines that must agree

RAG = Retrieval-Augmented Generation. Instead of relying only on what a language model memorized during training, you retrieve the most relevant chunks of your own documents at query time and paste them into the prompt, so the model answers from fresh, attributable, private context. The model is unchanged; the system around it does the work.

That system is really two pipelines with very different shapes. The write path (ingestion) runs offline: it reads source documents, splits them into chunks (chunking — cutting a long document into passages small enough to embed and retrieve individually), turns each chunk into a vector with an embedding model (a model that maps text to a fixed-length vector — say 768 or 1536 floats — so that semantically similar text lands nearby in that space), and stores those vectors in a vector database (a store that indexes embeddings for fast approximate nearest-neighbor (ANN) search — finding the closest vectors to a query vector without the prohibitive cost of comparing against every stored vector exactly). The read path (query) runs online, on the hot path: it embeds the user's question, searches the vector DB for the nearest chunks, optionally re-scores them with a reranker, assembles a prompt, and calls the LLM to generate an answer with citations.

write path is throughput-bound, offline, runs as Jobs/CronJobs — minutes to hours are fine
read path is latency-bound, online, runs as a Deployment behind a Service — a few hundred ms is the budget
the contract between them: same embedding model, same chunking, same index schema, same provenance — or retrieval quietly degrades

The mental model: a vector DB is a derived-data product, like a search index or a materialized view, not a pile of embeddings. It is computed from sources by a specific recipe. The instant the read path uses a different recipe than the one that built the index, the two pipelines are speaking different languages — and nothing crashes. This is why the book treats RAG as an application architecture: the model is one component, and the hardest bugs live in the consistency between write and read.

The binding contract
The two pipelines must agree on four things, versioned together as one unit: the embedding model (and its exact version), the chunker (size, overlap, boundary rules), the index schema (metric, dimensions, filter fields), and the source provenance (the record of where each chunk came from — which document, which version, which access-control tags — carried alongside the vector so an answer can cite its source and an auditor can trace any claim back to it). Change any one on the write side without re-running the read side to match, and similarity scores stop meaning what they meant.

2 · The write path — ingestion as Jobs and CronJobs

Ingestion is bulk GPU/CPU work that processes a whole corpus. It must never run inside the serving Deployment: a re-ingestion of millions of documents would consume the exact GPU capacity your users' queries need, and lesson 15's lesson applies directly — give ingestion lower priority and its own quota so it yields to online inference.

loadpull source documents (PDFs, wiki, tickets) and normalize to text; record source ID and version
chunksplit into passages with a size/overlap policy; small = precise, large = more context
embedrun each chunk through the embedding model on GPU; this is the expensive step
indexupsert vectors + metadata into the vector DB; build/refresh the ANN index
provenanceattach document ID, version, and access-control tags to every chunk

Kubernetes mapping: a one-time backfill is a Job; periodic refresh ("re-ingest changed docs nightly") is a CronJob; a complex multi-stage flow with retries and fan-out is a workflow engine (Argo Workflows, or a queue with worker Deployments). The embedding step needs model-serving capacity but lives off the hot path, so it can run on cheaper or spot GPUs and tolerate restarts — embedding is a pure function of (chunk, model version), so a preempted spot pod simply re-embeds the chunks it lost on restart with no corruption.

The embed step is the throughput bottleneck, and batching is the lever. Embedding models are small relative to an LLM, but a corpus is large, so the write path lives or dies on tokens-per-second. A single GPU running an embedding model in batches of 256 chunks might sustain ~200 chunks/s once the batch is full; at batch size 1 it manages a fraction of that because the GPU sits idle between requests. So ingestion deliberately accumulates chunks into large batches before calling the model — the opposite of the read path, which embeds one query at minimum latency. The two paths use the same model with opposite batching strategies, which is one more reason they are different workloads. For a 10M-chunk corpus at 200 chunks/s, the embed step alone is 10,000,000 ÷ 200 = 50,000 s ≈ 13.9 GPU-hours on one GPU — the same figure section 4 will charge you for any full re-ingestion.

Index writes are upserts, not appends. When a source document changes, you do not pile new vectors on top of the old ones — you re-chunk it, embed the new chunks, and upsert by a stable key (document ID + chunk index) so the new vectors replace the old, and you delete chunks for passages that vanished. Make this idempotent: re-running ingestion on an unchanged document must be a no-op (hash the chunk text and skip if the stored hash matches), so a retried or duplicated Job does not double-index the corpus. Without idempotency, every retry inflates the index and your nightly CronJob slowly fills the disk with stale duplicates.

apiVersion: batch/v1
kind: CronJob
metadata: { name: rag-reindex }
spec:
  schedule: "0 2 * * *"            # nightly, off-peak
  jobTemplate:
    spec:
      template:
        spec:
          priorityClassName: batch-low   # yields to online inference (lesson 15)
          containers:
          - name: ingest
            image: rag-ingest:emb-v3      # pinned embedding-model version
            env:
            - { name: EMBED_MODEL,  value: "text-embedding-v3" }
            - { name: CHUNK_TOKENS, value: "512" }
            - { name: CHUNK_OVERLAP, value: "64" }
            - { name: INDEX_SCHEMA, value: "schema-v3" }
            resources: { limits: { nvidia.com/gpu: 1 } }
          restartPolicy: OnFailure

Notice the version stamps live in the spec: the embedding model, chunk policy, and index schema are pinned together. The image tag emb-v3 and the schema schema-v3 move as one. That pinning is the whole game — it is what lets the read path declare which contract it expects and refuse to query an index built under a different one.

Chunking choiceBuysCosts
Small chunks (~128 tokens)Precise retrieval, tight citationsFragmented context; the model gets pieces, not the whole thought
Large chunks (~1024 tokens)More local context per hitNoisier retrieval; one chunk matches on an irrelevant sentence
Overlap between chunksSentences split across boundaries still retrievableMore vectors, larger index, higher embed cost

3 · The read path — query under a latency budget

The query path is a latency-sensitive Deployment behind a Service. Every stage spends part of a budget — say a 700 ms target for time-to-first-token. A typical breakdown:

embed querysame embedding model as ingestion → one short vector (~10-30 ms)
vector searchANN nearest-neighbor over the index, filtered by access-control tags → top-k chunks (~15-50 ms)
reranka cross-encoder re-scores the top-k for precision; keep top-n (~50-150 ms, optional)
assemblebuild the prompt from the kept chunks + their citations (~5 ms)
generateLLM produces the answer, streaming, with inline citations (the rest of the budget)

The reranker earns its latency by fixing a weakness of vector search. ANN retrieval scores each candidate by embedding similarity computed independently — the query was turned into one vector and each chunk into another, long before they ever met — so the score never sees the two pieces of text side by side. That is fast and gives good recall (the right chunk is usually somewhere in the top-50) but coarse precision (the single best chunk may be ranked 12th). A reranker is a cross-encoder: it feeds the query and one candidate into a model together, so attention can compare them token by token, and emits a sharper relevance score. The pattern is over-fetch then re-score: pull top-50 from ANN (cheap), rerank all 50 (one batched model call), keep top-5 for the prompt. You trade ~100 ms for materially better precision in what the model finally sees. Drop the reranker when the budget is tight and recall@5 is already high; keep it when wrong context is expensive — a confidently-cited but irrelevant chunk is worse than no chunk.

Two refinements worth naming. Hybrid search runs a keyword/lexical index (BM25) alongside the vector index and fuses the two rankings, because pure embedding search misses exact-match tokens — error codes, SKUs, names — that share no semantic neighborhood with the query. And top-k is a knob, not a constant: too small and the right chunk never enters the candidate set (a recall miss the reranker cannot fix); too large and you pay more rerank latency and risk diluting the prompt with marginal chunks. Tune k on the same fixed eval set you use to track recall.

Two non-negotiables on the read path. First, access-control filters must run at search time, not after: the vector query carries the user's permission tags so the ANN search only considers chunks the user is allowed to see. Filtering after retrieval is both a leak risk and a correctness bug — it risks the model reading and citing a document the user cannot access, and it also wrecks top-k, because if 8 of your top-10 ANN hits are filtered out post-hoc you are left with 2 chunks when the model needed 5. Push the filter into the index so the top-k you ask for is the top-k of permitted chunks. Second, every retrieved chunk carries its provenance, so the generated answer cites a real document and version, and an auditor can trace any sentence back to its source — a RAG answer with no resolvable citation is indistinguishable from a hallucination.

Worked latency number
Budget 700 ms TTFT. Embed query 20 ms + ANN search 40 ms + rerank 120 ms + assemble 5 ms leaves 700 − 185 = 515 ms for the LLM to produce the first token. If a corpus-wide reindex (section 2) were running on the same GPU pool, decode capacity would shrink and that 515 ms could blow past budget — which is exactly why ingestion runs as low-priority batch that yields to this path.

4 · The consistency trap and the cost of getting it wrong

Here is the failure that defines RAG operations. Suppose someone upgrades the embedding model on the ingestion side from v2 to v3 — better quality, everyone is happy — but the query service still embeds questions with v2. Now the stored vectors live in v3's space and the query vectors live in v2's space. Cosine similarity between two vectors from different embedding spaces is meaningless: it still returns a number, the search still returns "nearest" chunks, the answer still looks plausible. Nothing throws. Retrieval quality just quietly collapses, and you discover it weeks later from a slow drip of bad answers.

Why it is silent — and how to make it loud
There is no exception because both pipelines run fine in isolation; only their agreement broke. The defense is to version the embedding model, chunker, index schema, and provenance together and stamp that version on the index. The query service reads the index's version stamp at startup and refuses to serve if its own embedding-model version does not match. A mismatch becomes a loud startup failure instead of a silent quality regression.

The clean way to upgrade an embedding model, then, is a dual-index cutover: build a new index (schema-v3) with the new model while the live query path keeps serving the old one (schema-v2), and flip an alias only once the new index is fully built and its quality validated on the eval set. The query service is pinned to a model version and an index alias together, so it never embeds a v3 question against a v2 index. Cutover is one atomic alias swap; rollback is the reverse swap. This is the same pattern as a blue-green deploy, applied to derived data — and it is exactly the checkpoint exercise below.

The same coupling makes index loss expensive. Because the index is derived data, you can in principle rebuild it — but rebuilding means re-embedding the entire corpus, and that is not cheap.

Worked re-ingestion number
A 10M-chunk corpus. Re-embedding from scratch at, say, 5 ms/chunk on one GPU is 10,000,000 × 5 ms = 50,000 s ≈ 13.9 GPU-hours per GPU; even fanned across 20 GPUs it is ~42 minutes of full-throttle embed work, plus the index rebuild — and that whole window competes with serving. At ~$3/GPU-hour the embed alone is 13.9 × $3 ≈ $42 of compute, trivial in dollars but a real outage in capacity and time. So if your vector DB's index is lost and you have no tested restore, a routine disk failure becomes a multi-hour re-ingestion incident on shared GPUs — not a 60-second restore from a snapshot. This is why the next point matters more than its dollar figure suggests.

Therefore: operate the vector DB like the stateful system it is. It runs as a StatefulSet — the Kubernetes controller for pods that own durable, individually-identified storage, where each replica gets a stable name (vdb-0, vdb-1) and its own PersistentVolume that survives reschedules, unlike the interchangeable, disk-less pods of a Deployment. That stable identity is what lets a vector shard keep its data when its pod restarts. On top of it you need the full stateful discipline:

Backup
Snapshot the volumes (or use the DB's native snapshot) on a schedule, off-peak, to object storage. A backup is the only thing standing between an index loss and the 14-GPU-hour re-ingestion above.
Tested restore
A backup you have never restored is a hypothesis, not a recovery plan. Restore into a scratch namespace on a cadence and check vector count + recall@k — so the first restore is a drill, not the incident.
Compaction
Upserts and deletes leave tombstoned vectors that bloat the index and slow ANN search. Compaction rebuilds segments to reclaim space; schedule it like ingestion (off-peak, low priority) since it is I/O- and CPU-heavy.
Scaling
As the corpus grows, shard (split vectors across replicas — more total capacity) or replicate (copy a shard across replicas — more query throughput and HA). Resharding moves data, so plan it before the disk is full, not after.
Tenant isolation
Separate namespaces/collections per tenant (or a strict tenant filter) so one customer's queries, quotas, and a noisy reindex cannot bleed into another's latency — and so a filter bug cannot return another tenant's chunks.

An untested restore is the difference between a 60-second snapshot recovery and the multi-hour re-ingestion incident above. The dollar cost of re-embedding is trivial; the cost is the capacity and the downtime, on the very GPUs your users are queued behind.

5 · Failure modes and what reveals them

Failure modes

  • Embedding-model mismatch (write vs. read). Ingestion upgrades the embedding model but the query path does not, so stored and query vectors live in different spaces and similarity scores degrade silently — no error, just worse answers. Signal: retrieval-quality metrics (hit-rate, recall@k on a fixed eval set) drop while everything stays green; an index-version check at query startup would have caught it first.
  • Untested vector-DB restore. Index loss (disk failure, bad compaction, accidental delete) forces a full re-embed of the corpus because no snapshot was ever restored successfully. Signal: the first time anyone runs the restore is during the incident, and it fails or is missing — turning minutes into hours on shared GPUs.
  • Citations without provenance or ACL checks. Chunks are stored or returned without a document ID/version or without filtering by the user's permissions, so an answer cites a non-existent source or leaks a restricted document. Signal: citations that do not resolve to a current source, or an access-review finding a user could retrieve data they cannot open directly.
  • Stale index drift. Sources change but re-ingestion does not keep up, so answers cite outdated versions. Signal: growing gap between source last-modified and chunk ingested-at timestamps.
  • Ingestion starves serving. A reindex runs at full GPU priority and inflates query latency past budget. Signal: TTFT spikes correlated with reindex windows; fixed by batch priority/quota (lesson 15).

Implementation checklist

  • Are the embedding model, chunker, index schema, and source provenance versioned together as one unit, with the version stamped on the index?
  • Does the query service verify its embedding-model version against the index's at startup and refuse to serve on mismatch?
  • Does ingestion run as Jobs/CronJobs at batch priority with its own quota, so it yields to online serving?
  • Is the vector DB a StatefulSet (or managed) with backup, a tested restore, compaction, a scaling plan, and tenant isolation?
  • Do access-control filters run at search time, not after retrieval, and does every chunk carry provenance?
  • Are retrieval quality (recall@k on a fixed eval set) and generation quality measured and tracked separately?
  • Is there a known re-ingestion runbook with its time/GPU cost estimated, so index loss is a planned recovery, not a surprise?

Checkpoint exercise

Try it
You are upgrading your embedding model from v2 to v3 on a 10M-chunk corpus with zero allowed quality regression and no read downtime. Sketch the rollout: how do you re-embed the corpus into a new index without touching the live v2 index, how does the query service know which index to hit, how do you cut over atomically, and what version check guarantees a query embedded with v3 never hits the v2 index? Estimate the GPU-hours and the batch priority you would give the reindex Job.

Where this points next

RAG gives a model trustworthy context for a single question-and-answer turn. But an agent does not stop at one retrieval — it plans, calls tools, observes results, and loops, and retrieval becomes just one tool it can choose to invoke (often several times, with refined queries). Lesson 17, Agents on Kubernetes, adds the machinery for that loop: long-running stateful sessions, tool-call orchestration, concurrency and memory limits, and the scheduling implications of workloads that fan out unpredictably. The write/read consistency discipline you built here stays — the agent's retrieval tool is exactly this RAG read path — but now it sits inside a controller that decides when to retrieve.

Takeaway
RAG is two coupled pipelines, not a model plus a database. The write path (load → chunk → embed → index → provenance) runs as throughput-bound Jobs/CronJobs at batch priority so it never starves serving; the read path (embed → search → rerank → assemble → generate) runs as a latency-bound Deployment that filters by access control at search time and cites real provenance. They are bound by a contract — the same embedding model, chunker, index schema, and provenance — that you must version together, because a write/read mismatch degrades retrieval silently with no error to catch it. The vector DB is a stateful derived-data product: run it as a StatefulSet with backup, a tested restore, compaction, scaling, and tenant isolation, because index loss without a working restore becomes a full re-ingestion incident on shared GPUs. Measure retrieval quality and generation quality separately so you can tell which half regressed.

Interview prompts