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.
Where Where to implement
Jetson terminal (tmux window "llm")
- Build llama.cpp with CUDA
- Run
llama-serveron127.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.
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-server2Download 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/.
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 Dcurl -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.
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.
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
# 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
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")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
| Symptom | Fix |
|---|---|
| Build runs out of memory | Use -j2; make sure swap is on; stop JupyterLab kernels while building. |
| Server is slow or uses the CPU | Rebuild with -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=87; check the log reports layers offloaded to the GPU. |
| Out of memory when the model loads | Check what else is running in jtop (a training kernel, a second LLM); lower --ctx-size to 4096. |
| cmake can't find CUDA | Pass 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 numbers | Lower the temperature to 0, shorten the JSON, and repeat "use only these numbers". |