Free course · by the SutraCore team

Free NVIDIA Jetson AI course: 30 days, 16 steps

Orin NX 16GBJetPack 6.2.1CUDA 12.6TensorRT 10.3Python 3.10PyTorch 2.8
Step 11 of 16Day 19 · Fri 13 NovWeek 3Phase A · notebooks

Nemotron advisor and RAG

Run NVIDIA's Nemotron 3 Nano 4B on the Jetson, use it to explain training results in plain language, and build a small RAG that answers questions from your own notes with sources.

Runs on
Jetson terminal (llama.cpp server) + JupyterLab (client)
Notebooks
60_nemotron_advisor.ipynb, 61_rag_basics.ipynb
Creates
~/llama.cpp/build, models/NVIDIA-Nemotron3-Nano-4B-Q4_K_M.gguf, reports/rag_eval.json
Time
Day 19 (the build takes about 30–60 min)

Where Where to implement

Jetson terminal (tmux window "llm")

  • Build llama.cpp with CUDA
  • Run llama-server on 127.0.0.1:8080

Notebooks

  • Call the server with the OpenAI Python client
  • The 4B model uses about 3–3.5 GB of your 16 GB, so it can run next to a small training job. Watch jtop.

Do Do it

1Build llama.cpp for the Orin GPU

Orin's GPU is compute capability 8.7 (jtop: "CUDA Arch BIN: 8.7"), so you build for architecture 87. -j6 uses six of your eight cores and leaves RAM for the compiler.

Jetson terminal
sudo apt install -y cmake build-essential
cd ~ && git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=87 -DCMAKE_CUDA_COMPILER=/usr/local/cuda-12.6/bin/nvcc
cmake --build build --config Release -j6
ls build/bin/llama-server

2Download the model and start the server

Get NVIDIA-Nemotron3-Nano-4B-Q4_K_M.gguf from the download link on its Jetson AI Lab model page, and save it in ~/sutraai/models/.

Jetson terminal
tmux new -s llm
~/llama.cpp/build/bin/llama-server -m ~/sutraai/models/NVIDIA-Nemotron3-Nano-4B-Q4_K_M.gguf \
  --alias nemotron --n-gpu-layers 999 --ctx-size 8192 --host 127.0.0.1 --port 8080
# leave it running; detach with Ctrl+B then D
Jetson terminal
curl -s http://127.0.0.1:8080/v1/chat/completions -H "Content-Type: application/json" \
  -d '{"model":"nemotron","messages":[{"role":"user","content":"Say hello in five words."}]}'

3NB60: the Training Advisor

Rule: the code computes every number, and the LLM only explains them. Tell it so in the prompt.

Notebook cellnotebooks/60_nemotron_advisor.ipynb
import json, time
from pathlib import Path
from openai import OpenAI
ROOT = Path.home() / "sutraai"
client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="not-needed")

def advise(metrics_path):
    metrics = json.load(open(metrics_path))
    prompt = ("You are a training advisor for small edge-AI models. Using ONLY the numbers in this JSON, "
              "explain the results to a beginner in 5 short bullet points, then suggest the 2 most useful next steps. "
              "Never invent numbers.\n\n" + json.dumps(metrics))
    t = time.perf_counter()
    r = client.chat.completions.create(model="nemotron", temperature=0.2,
                                       messages=[{"role": "user", "content": prompt}])
    dt = time.perf_counter() - t
    text = r.choices[0].message.content
    toks = r.usage.completion_tokens if r.usage else None
    return text, dt, toks

text, dt, toks = advise(ROOT / "reports/audio_v1/metrics.json")
print(text); print(f"\n{toks} tokens in {dt:.1f} s" + (f" = {toks/dt:.1f} tokens/s" if toks else ""))

Check that every number in the answer exists in metrics.json. Watch RAM in jtop while it answers and write both numbers in BUILDLOG.

4NB61: RAG over your own notes

Retrieval-augmented generation: find the most relevant text first, then let the LLM answer only from it, with citations.

Notebook cellnotebooks/61_rag_basics.ipynb
import numpy as np
from pathlib import Path
from openai import OpenAI
from sentence_transformers import SentenceTransformer
ROOT = Path.home() / "sutraai"

def chunk_markdown(text, max_chars=1500):
    parts, cur = [], ""
    for para in text.split("\n\n"):
        if len(cur) + len(para) > max_chars and cur:
            parts.append(cur.strip()); cur = ""
        cur += para + "\n\n"
    if cur.strip(): parts.append(cur.strip())
    return parts

sources = [ROOT / "BUILDLOG.md"] + sorted((ROOT / "docs").glob("*.md"))     # copy the SutraAI .md docs into ~/sutraai/docs
chunks, origin = [], []
for src in sources:
    if src.exists():
        for c in chunk_markdown(src.read_text()):
            chunks.append(c); origin.append(src.name)

emb = SentenceTransformer("BAAI/bge-small-en-v1.5", device="cuda")    # small and fast; try NVIDIA's embedder next
E = emb.encode(chunks, normalize_embeddings=True)

def retrieve(q, k=4):
    s = E @ emb.encode([q], normalize_embeddings=True)[0]
    return list(s.argsort()[::-1][:k])

client = OpenAI(base_url="http://127.0.0.1:8080/v1", api_key="not-needed")
def ask(q):
    ids = retrieve(q)
    context = "\n\n".join(f"[{n+1}] ({origin[i]}) {chunks[i]}" for n, i in enumerate(ids))
    prompt = (f"Answer the question using ONLY the numbered sources. Cite them like [1]. "
              f"If the sources don't contain the answer, say so.\n\nSources:\n{context}\n\nQuestion: {q}")
    r = client.chat.completions.create(model="nemotron", temperature=0.1, messages=[{"role": "user", "content": prompt}])
    return r.choices[0].message.content, [origin[i] for i in ids]

print(ask("How do I convert a model to int8 TFLite?"))

5Measure retrieval quality

Notebook cellnotebooks/61_rag_basics.ipynb
# 10 questions you know the answer to + a phrase that must appear in the right chunk
tests = [
    ("Which power mode do I use for training?", "MAXN"),
    ("How big should swap be?", "16"),
    ("What is the int8 accuracy budget?", "3 points"),
    # ... add 7 more from your own BUILDLOG
]
hits = sum(any(phrase.lower() in chunks[i].lower() for i in retrieve(q)) for q, phrase in tests)
print(f"hit@4 = {hits}/{len(tests)}")

Then try NVIDIA's llama-nemotron-embed-1b-v2 embedding model (follow its Hugging Face model card) in place of bge-small and compare hit@4. It uses about 2.5 GB, which fits next to Nemotron on 16 GB.

6Optional: a bigger NVIDIA model, because you have 16 GB

An Orin Nano (8 GB) stops at about 4B parameters. Your Orin NX can try Nemotron Nano 9B v2 as a 4-bit GGUF (roughly 6 GB). Run it on its own, compare answer quality and tokens/s with the 4B model, and check its Jetson AI Lab page for an Orin NX build first. Keep the 4B model as the default if the difference is small. It's faster and leaves room for training.

Test Test it

Notebook celllast cells of NB60 and NB61
assert toks and toks / dt > 3, "very slow: check the server log shows the model on the GPU (offloaded layers)"
assert hits >= 0.8 * len(tests), f"hit@4 {hits}/{len(tests)}: try smaller chunks or better questions"
print("✅ Nemotron advisor and RAG OK")
Pass: the advisor's numbers all come from metrics.json, tokens/s and RAM are recorded, RAG answers cite sources, and hit@4 ≥ 8/10.

Done Done when

  • llama.cpp built with CUDA (arch 87)
  • Nemotron 3 Nano 4B answers through the OpenAI-style API
  • Advisor explains metrics.json without inventing numbers
  • tokens/s and peak RAM in BUILDLOG
  • RAG answers cite chunks; hit@4 ≥ 8/10
  • Tried (or scheduled) the NVIDIA embedder comparison

If it breaks

SymptomFix
Build runs out of memoryUse -j2; make sure swap is on; stop JupyterLab kernels while building.
Server is slow or uses the CPURebuild with -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=87; check the log reports layers offloaded to the GPU.
Out of memory when the model loadsCheck what else is running in jtop (a training kernel, a second LLM); lower --ctx-size to 4096.
cmake can't find CUDAPass the compiler explicitly (already in the command above): /usr/local/cuda-12.6/bin/nvcc. Check nvcc --version shows V12.6.68.
Prefer a ready-made container?On JetPack 6.2.1 (L4T r36.4), jetson-containers images for L4T r36.4 match your board: jetson-containers run $(autotag llama_cpp).
Advisor invents numbersLower the temperature to 0, shorten the JSON, and repeat "use only these numbers".

Learn more