hands-on labon a real NVIDIA GPU

Retrieval-Augmented Generation (RAG) Pipeline with Local Models

Build an end-to-end RAG pipeline on a single GPU: BGE embeddings, L2-normalized vector retrieval by dot product, and a local generator that answers with and without retrieved context so you can see exactly what retrieval changes.

time
45 min
checked steps
4
level
Intermediate
setup
None

$29.99a month with Pro

Pro opens this lab and every other one. Cancel any time.

Lab cockpit45 min · 4 stepsSession running
3 / 4 steps passingGenerate with and without retrieval context · step 4 of 4
lab.ipynb

Step 4 — Generate with and without retrieval context

The moment of truth: ask the LLM the same question with and without retrieved context, compare.

[ ]:
print('Loading TinyLlama-1.1B-Chat for generation...')GEN_ID = 'TinyLlama/TinyLlama-1.1B-Chat-v1.0'gen_tokenizer = AutoTokenizer.from_pretrained(GEN_ID)gen_model = AutoModelForCausalLM.from_pretrained(GEN_ID, torch_dtype=torch.float16).to(device)gen_model.eval() def generate_answer(prompt, max_new=120):    ids = gen_tokenizer(prompt, return_tensors='pt').input_ids.to(device)    with torch.no_grad():        out = gen_model.generate(            ids, max_new_tokens=max_new, do_sample=False,            pad_token_id=gen_tokenizer.eos_token_id,        )    return gen_tokenizer.decode(out[0, ids.shape[1]:], skip_special_tokens=True).strip() print('OK — generator loaded')
[ ]:
# No-RAG: just ask the questionno_rag_prompt = f"Q: {query}\nA:"no_rag_answer = generate_answer(no_rag_prompt) # RAG: inject retrieved contextcontext = '\n\n'.join(top_chunks)rag_prompt = (    f"Use the following context to answer the question.\n\n"    f"CONTEXT:\n{context}\n\n"    f"QUESTION: {query}\n\n"    f"ANSWER:")rag_answer = generate_answer(rag_prompt) print('=' * 70)print(f'QUERY: {query}')print('=' * 70)print(f'\nNO-RAG ANSWER (TinyLlama alone):')print(f'  {no_rag_answer}')print(f'\nRAG ANSWER (same LLM + retrieved context):')print(f'  {rag_answer}')print('\n' + '=' * 70)print('The RAG answer should reference 15 trillion tokens (from our corpus). The no-RAG answer')print('may hallucinate or give a generic response — TinyLlama-1.1B has no memory of Llama 3 numbers.')
[ ]:
lab.check(4)
step 4 of 4, replayed: Generate with and without retrieval context. The code, the output and the check are the lab's own.

4 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Build a corpus + load the embedding model

    read this step below ↓

  2. 2

    Chunk the documents, embed the chunks

  3. 3

    Retrieval: top-K nearest chunks

  4. 4

    Generate with and without retrieval context

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Build a corpus + load the embedding model

Language models have a frozen worldview: they only know what was in their pretraining corpus. Ask GPT-2 about a 2024 event and it hallucinates. Retrieval-Augmented Generation fixes this at the cheapest possible place in the stack — you look up relevant facts and paste them into the prompt. No fine-tuning, no retraining, just embed + search + prompt.

RAG is the single most widely-deployed LLM pattern in 2024–2026. Every enterprise "chat with your docs" product, every helpdesk bot, every search-grounded answer in ChatGPT and Claude and Gemini — all RAG underneath. The Lewis et al. 2020 paper introduced the term; the whole ecosystem (LangChain, LlamaIndex, pgvector, Milvus, Weaviate, Pinecone) exists to make this one pipeline easier to build.

The pipeline in five moves

documents → [chunker] → chunks → [embedding model] → vectors (in a store)
                                                              ↓
          query → [embedding model] → vector  → [similarity search]
                                                              ↓
                              top-K chunks + query → [LLM] → grounded answer

Every knob matters:

  • Chunk size — too small = missing context, too big = diluted signal. 256–512 tokens is typical.
  • Embedding model — BGE, E5, Jina, and OpenAI's ada-002 are common. All produce ~768-1024-dim vectors.
  • Retrieval K — top-3 to top-10 chunks is standard. More = more context but more noise.
  • Vector store — in-memory matrix (this lab), FAISS, Milvus, pgvector. Trade-off: latency vs scale.
  • Prompt template — how you inject the retrieved context affects the LLM's willingness to use it.

References to read alongside

Models

  • Embedding: BAAI/bge-small-en-v1.5 (~130 MB, 384-dim vectors, produced by the BGE team)
  • Generator: TinyLlama-1.1B-Chat (~2 GB, used throughout this course)

Our knowledge base: 8 carefully-chosen facts about modern LLMs. This is tiny but lets you see RAG end-to-end cleanly. In production your corpus is tens of thousands to millions of documents and the retrieval step has to be fast (milliseconds).

import torch
import torch.nn.functional as F
from transformers import AutoModel, AutoTokenizer, AutoModelForCausalLM

device = torch.device('cuda')

documents = [
    "Llama 3 was released by Meta in April 2024. It was trained on approximately 15 trillion tokens — roughly 5x more data than Llama 2. The 8B and 70B variants are open-weight.",
    "The Transformer architecture was introduced by Vaswani et al. in the 2017 paper 'Attention Is All You Need'. It replaced recurrence and convolution with pure attention, kicking off the modern LLM era.",
    "GPT-3, released by OpenAI in 2020, has 175 billion parameters. Its emergent few-shot learning abilities surprised researchers and marked a major scaling-law inflection point.",
    "LoRA (Low-Rank Adaptation) was proposed by Hu et al. in 2021. It lets you fine-tune a frozen pretrained model by adding tiny low-rank matrices on top of attention layers, typically cutting trainable parameters by 10000x.",
    "The BGE family of embedding models was released by the Beijing Academy of Artificial Intelligence in 2023. BGE-small has 33 million parameters and produces 384-dimensional vectors.",
    "Mixture of Experts (MoE) models use many specialist subnetworks and a router that sends each token to a subset. Mixtral 8x7B (released December 2023 by Mistral) demonstrated MoE at competitive quality.",
    "The Chinchilla paper (Hoffmann et al., 2022) established compute-optimal training: models should see roughly 20 tokens per parameter during pretraining. Most pre-Chinchilla models were undertrained.",
    "Retrieval-Augmented Generation was formalized in the 2020 RAG paper by Lewis et al. It combines a parametric language model with a non-parametric knowledge store accessed via dense retrieval.",
]
print(f'{len(documents)} documents in corpus')
EMBED_ID = 'BAAI/bge-small-en-v1.5'
print(f'Loading embedding model {EMBED_ID} (~130 MB first time)...')
embed_tokenizer = AutoTokenizer.from_pretrained(EMBED_ID)
embed_model = AutoModel.from_pretrained(EMBED_ID).to(device)
embed_model.eval()

# The idiomatic BGE encode: tokenize, forward pass, take [CLS] or mean-pool, then L2-normalize.
# BGE's team recommends [CLS] pooling specifically (not mean pool) for this model family.
def embed(texts, max_len=256):
    """Encode a list of strings into L2-normalized 384-dim vectors on CUDA."""
    enc = embed_tokenizer(texts, padding=True, truncation=True, max_length=max_len, return_tensors='pt').to(device)
    with torch.no_grad():
        out = embed_model(**enc)
    # BGE uses [CLS] pooling: the first token's hidden state at the last layer
    pooled = out.last_hidden_state[:, 0]
    # L2-normalize so cosine similarity reduces to dot product
    return F.normalize(pooled, dim=-1)

# Quick smoke test
v = embed(['What is Llama 3?', 'How big is Llama 3?'])
print(f'Encoded shape: {tuple(v.shape)}, norm: {torch.norm(v, dim=-1).cpu().tolist()}')
similar = (v[0] @ v[1]).item()
print(f'Cosine sim between two Llama-3 questions: {similar:.3f} (should be > 0.8 — they are semantically close)')

what you get

  • A JupyterLab notebook on a real NVIDIA GPU
  • A check at the end of every step
  • Hints and the full solution
  • Nothing to install or configure

before you start

  • Python + PyTorch basics
  • Familiarity with transformer embeddings and tokenization
  • Basic linear algebra (dot product, cosine similarity)

Exam domains covered

Retrieval-Augmented Generation & Vector SearchLLM Application Development

Part of the RAG and search track

Lab 2 of 14. Chunking, hybrid search and reranking, permission-aware retrieval, GraphRAG and RAG evaluation.

Questions about this lab

Why use CLS pooling with BGE instead of mean pooling?

BGE was trained with a dedicated CLS objective, the [CLS] token's final hidden state is what the contrastive loss shaped into a sentence embedding. Mean pooling over all token states is common for other encoders (sentence-BERT's bert-base-nli variants use it), but for BGE specifically it underperforms because those token states weren't optimized for pooling. Always check the model card: swapping pooling strategies between encoders is one of the silent ways to ship a subtly broken retriever.

Why L2-normalize the embeddings before retrieval?

After normalization every vector has unit length, so a @ b.T produces values in [-1, 1] that equal cos(a, b) exactly. You get cosine similarity from a single matmul, no separate norm division, no numerical drift, and torch.topk sorts correctly on the raw scores. Skipping normalization doesn't break retrieval but forces you to divide by norms at query time and silently makes short passages look more similar than they should.

How is this lab different from the Advanced RAG lab?

This one is the end-to-end baseline: dense retrieval, top-K, generation with and without context, the minimum viable pipeline, focused on correctness and on showing you that retrieval actually changes the answer. Advanced RAG adds BM25, Reciprocal Rank Fusion for hybrid search, and a cross-encoder reranker on top, the two-stage retrieval pattern used in production enterprise RAG. Do this one first; take that one when you want to see where the recall and precision gaps come from.

Why does the lab not use a vector database like FAISS, Milvus, or pgvector?

Because the corpus fits in a single CUDA tensor, and at that scale a @ matmul is faster and clearer than any ANN index. You want to understand that retrieval is ultimately a distance computation over a matrix, then the vector databases, graph indices, quantization tricks, and sharding schemes that ship at production scale are recognizable as engineering optimizations over this exact primitive, not mysterious black boxes.

What happens if rag_answer equals no_rag_answer?

The grader fails Step 4 and tells you retrieval had no effect. The usual causes: the prompt template ignores the injected context (common with chat-formatted generators where the system message gets stripped), the top-K chunks didn't contain the answer so the model fell back to priors, or max_new_tokens is so low both answers are generic. The reflection step pushes you to separate these: chunker bug, embedder bug, retriever bug, or generator bug, each with a different fix.

Can I swap BGE for a different embedding model?

Yes, and it's an instructive exercise. Try intfloat/e5-small-v2 or sentence-transformers/all-MiniLM-L6-v2 and watch whether retrieval quality changes on your specific corpus. You'll need to match the pooling strategy to the model (E5 and BGE use CLS; MiniLM uses mean) and re-check that output vectors are L2-normalized. The pipeline code otherwise doesn't change, which is the whole point of treating the embedder as a pluggable component.

What you'll build in this end-to-end RAG lab

RAG is the production must-know for anyone adding LLMs to a real product, retrieval is the mechanism that lets a frozen model talk about your data, this week's data, or data too large to fit in any context window. In 45 minutes you'll stand up a complete Retrieval-Augmented Generation pipeline on a single GPU with no vector database and no orchestration framework, just the primitives underneath every production stack: a BGE bi-encoder, an L2-normalized passage index that lives as one CUDA tensor, dot-product top-K retrieval via a single matmul, and a local causal LM that answers the same question with and without retrieved context so you can see exactly what retrieval changes. You'll walk away with a clear mental model of why dot product equals cosine similarity on unit-norm vectors, why every FAISS/Milvus/pgvector index is an engineering optimization over this exact primitive, and a diagnostic framework for the four places RAG pipelines break (chunker, embedder, retriever, generator).

The technical substance is where each primitive comes from and why. BGE was trained with a dedicated CLS objective, so the [CLS] token's final hidden state is the sentence embedding, mean pooling silently underperforms even though it's the default for sentence-BERT style encoders. L2-normalization isn't decoration: it makes a @ b.T produce values in [-1, 1] that equal cos(a, b) exactly, which means torch.topk sorts correctly on raw scores without a separate norm division. Step 4's grader requires rag_answer != no_rag_answer so you can't get credit for retrieval that didn't actually condition generation, and when they do differ, the reflection asks you to diagnose whether retrieval changed the answer by grounding it or just by perturbing it, which is the real question. You'll also see the operational points engineers learn the hard way: high cosine similarity can still surface irrelevant passages when query and corpus are paraphrased away from each other, a bad retriever gives the model confident-looking wrong context to hallucinate from, and 'RAG fixes hallucinations' is only true when recall@k and faithfulness are measured separately.

Prerequisites are Python plus PyTorch basics, familiarity with transformer tokenization, and enough linear algebra to know what a dot product is. The sandbox is a real NVIDIA GPU pod we provision per session with BGE, the generator model, tokenizers, and CUDA preinstalled. Checks are strict about correctness, L2-normalized embeddings (unit norm within 1e-3), passage vectors on GPU with dimensions matching the chunk count, top-K ordering descending with the top-1 for a Llama-3-tokens query actually retrieving a relevant chunk, and the final answer grader failing if rag_answer == no_rag_answer. Once you have this baseline working, the Advanced RAG lab adds BM25, Reciprocal Rank Fusion for hybrid search, and cross-encoder reranking on top.