track
NVIDIA Nemotron-3 este noua familie de modele deschise de la NVIDIA, creată pentru raționament, programare, chat și fluxuri de lucru AI agentice. Include diverse dimensiuni de model, precum Nano, Super și Ultra, astfel încât dezvoltatorii pot alege între modele mai mici și eficiente și modele mai mari, cu performanțe ridicate.
Actualizarea esențială la Nemotron-3 este accentul pe eficiență. Modelele sunt proiectate să ofere performanțe solide, păstrând în același timp inferența și ajustarea fină la un nivel practic. Versiunea Nano este deosebit de utilă pentru experimente practice, deoarece poate rula pe configurații GPU mai accesibile comparativ cu modelele mai mari.
În acest ghid, vom ajusta fin NVIDIA Nemotron-3-Nano-4B pe un set de date de întrebări și răspunsuri din psihologie. Vom folosi Low-Rank Adaptation (LoRA), Transformers Reinforcement Learning (TRL) și Hugging Face pentru a pregăti datele, antrena modelul, salva adaptorul, îl încărca pe Hugging Face și compara răspunsurile înainte și după ajustarea fină.
Pentru a începe să găsiți cele mai noi modele AI open-source, să construiți agenți AI și să faceți ajustări fine de LLM-uri, recomand înscrierea în traseul nostru de competențe Hugging Face Fundamentals.
1. Configurarea mediului
Nemotron-3 Nano folosește o arhitectură hibridă, așadar pachetele legate de Mamba trebuie instalate corect. Într-un notebook Jupyter, mai întâi eliminăm stiva existentă PyTorch și reinstalăm versiunea PyTorch 2.7.1 pentru CUDA 12.8, care funcționează cu versiunile fixate mamba_ssm și causal_conv1d folosite aici.
Instalăm, de asemenea, bibliotecile esențiale pentru ajustare fină, inclusiv transformers, trl, accelerate, datasets, peft și huggingface_hub.
%%capture
!pip install -U packaging ninja
# Replace the current PyTorch stack with the CUDA 12.8 build that works with these Mamba kernel pins.
!pip uninstall -y torch torchvision torchaudio triton
!pip install "torch==2.7.1" "torchvision==0.22.1" "torchaudio==2.7.1" --index-url https://download.pytorch.org/whl/cu128
!pip install -U "transformers==4.56.2" tokenizers "trl==0.22.2" accelerate datasets peft pandas tqdm huggingface_hub safetensors
!pip install -U --no-build-isolation "mamba_ssm==2.2.5" "causal_conv1d==1.5.2"
După instalarea pachetelor, verificați că CUDA este disponibil și că PyTorch detectează GPU-ul. Acest notebook este ajustat pentru un GPU de 24GB, astfel încât vă va avertiza dacă GPU-ul are mai puțină VRAM.
import os
import platform
import torch
print(f"Python: {platform.python_version()}")
print(f"PyTorch: {torch.__version__}")
print(f"PyTorch CUDA build: {torch.version.cuda}")
print(f"CUDA available: {torch.cuda.is_available()}")
if not torch.cuda.is_available():
raise RuntimeError(
"CUDA is not available. Select a RunPod PyTorch image with GPU support."
)
for idx in range(torch.cuda.device_count()):
props = torch.cuda.get_device_properties(idx)
total_gb = props.total_memory / 1024**3
print(
f"GPU {idx}: {props.name} ({total_gb:.1f} GB VRAM, capability {props.major}.{props.minor})"
)
if torch.cuda.get_device_properties(0).total_memory < 24 * 1024**3:
print(
"Warning: this 4B LoRA notebook is tuned for GPUs with at least 24GB VRAM. Reduce batch sizes on smaller GPUs."
)
torch.backends.cuda.matmul.allow_tf32 = True
torch.backends.cudnn.allow_tf32 = True
Rezultat:
Python: 3.12.3
PyTorch: 2.7.1+cu128
PyTorch CUDA build: 12.8
CUDA available: True
GPU 0: NVIDIA GeForce RTX 3090 (23.6 GB VRAM, capability 8.6)
Warning: this 4B LoRA notebook is tuned for GPUs with at least 24GB VRAM. Reduce batch sizes on smaller GPUs.
Setați tokenul dumneavoastră Hugging Face ca variabilă de mediu denumită HF_TOKEN. Acest lucru permite notebook-ului să descarce modelul Nemotron-3 și, ulterior, să încarce adaptorul LoRA ajustat fin pe Hugging Face.
from huggingface_hub import login
hf_token = os.environ.get("HF_TOKEN")
if not hf_token:
raise ValueError(
"Set HF_TOKEN in the RunPod environment before running this notebook."
)
login(token=hf_token)
print("Logged in to Hugging Face.")
2. Încărcarea și procesarea setului de date
În continuare, vom încărca setul de date de întrebări și răspunsuri din psihologie de pe Hugging Face. Setul de date conține o coloană question și două coloane de răspuns: response_j și response_k. Pentru acest ghid, vom folosi response_j ca răspuns țintă pentru ajustarea fină supervizată.
Mai întâi încărcăm setul de date, îl amestecăm pentru reproductibilitate și creăm împărțiri pentru antrenare, validare și testare.
from datasets import DatasetDict, load_dataset
DATASET_ID = "jkhedri/psychology-dataset"
TRAIN_LIMIT = 8000
VALIDATION_LIMIT = 800
TEST_LIMIT = 300
SEED = 42
raw_dataset = load_dataset(DATASET_ID)
raw_train = raw_dataset["train"].shuffle(seed=SEED)
split_1 = raw_train.train_test_split(test_size=0.15, seed=SEED)
split_2 = split_1["test"].train_test_split(test_size=0.33, seed=SEED)
def maybe_limit(split, limit):
if limit is None:
return split
return split.select(range(min(limit, len(split))))
dataset = DatasetDict(
{
"train": maybe_limit(split_1["train"], TRAIN_LIMIT),
"validation": maybe_limit(split_2["train"], VALIDATION_LIMIT),
"test": maybe_limit(split_2["test"], TEST_LIMIT),
}
)
dataset
Rezultat:
DatasetDict({
train: Dataset({
features: ['question', 'response_j', 'response_k'],
num_rows: 8000
})
validation: Dataset({
features: ['question', 'response_j', 'response_k'],
num_rows: 800
})
test: Dataset({
features: ['question', 'response_j', 'response_k'],
num_rows: 300
})
})
Înainte de a formata setul de date pentru antrenare, verificați denumirile coloanelor și vizualizați un exemplu. Acest lucru confirmă că setul de date a fost încărcat corect și conține câmpurile așteptate pentru întrebare și răspuns.
dataset["train"].column_names, dataset["train"][0]
Rezultat:
(
['question', 'response_j', 'response_k'],
{
'question': "I'm experiencing anxiety about social situations and don't know how to cope.",
'response_j': "Social anxiety can be a difficult and isolating experience, but there are effective treatments available. Let's work on developing coping mechanisms, such as deep breathing and mindfulness, and exposure therapy to gradually confront your fears. We can also explore ways to improve social skills and build self-confidence.",
'response_k': "Just avoid social situations. It's not worth the anxiety and discomfort. You can also try using alcohol or drugs to help you feel more comfortable in social settings."
}
)
3. Formatarea setului de date pentru ajustare fină cu TRL
Acum vom converti setul de date în formatul prompt-completion așteptat de TRL. Fiecare exemplu va include un prompt de sistem, întrebarea utilizatorului din domeniul psihologiei și răspunsul țintă al asistentului din response_j.
Promptul de sistem îi spune modelului cum să răspundă: să fie susținător, să evite urmele de raționament ascunse, să ofere sugestii practice și să evite să se prezinte ca profesionist autorizat în sănătate mintală.
SYSTEM_PROMPT = """/no_think
You are a supportive psychology question-answering assistant.
Do not include hidden reasoning, thinking traces, <think> tags, or </think> tags in the final answer.
Respond with empathy, practical coping suggestions, and clear next steps.
Give a complete answer in 2-4 short paragraphs or a brief paragraph plus 3-5 practical bullets.
Do not diagnose the user or claim to replace a licensed mental health professional.
If the user may be in immediate danger or crisis, encourage contacting local emergency services or a trusted crisis hotline.
Keep the answer safe, specific, and directly relevant to the user's question without being overly brief."""
CHAT_TEMPLATE_KWARGS = {"enable_thinking": False}
USER_TEMPLATE = "Question:\n\n{question}"
def clean_text(value):
return " ".join(str(value).strip().split())
def to_prompt_completion(example):
question = clean_text(example["question"])
answer = clean_text(example["response_j"])
return {
"prompt": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": USER_TEMPLATE.format(question=question)},
],
"completion": [
{"role": "assistant", "content": answer},
],
"chat_template_kwargs": CHAT_TEMPLATE_KWARGS,
}
sft_dataset = dataset.map(
to_prompt_completion, remove_columns=dataset["train"].column_names
)
sft_dataset["train"][0]
Rezultat:
{
'prompt': [
{
'role': 'system',
'content': "/no_think\nYou are a supportive psychology question-answering assistant.\nDo not include hidden reasoning, thinking traces, <think> tags, or </think> tags in the final answer.\nRespond with empathy, practical coping suggestions, and clear next steps.\nGive a complete answer in 2-4 short paragraphs or a brief paragraph plus 3-5 practical bullets.\nDo not diagnose the user or claim to replace a licensed mental health professional.\nIf the user may be in immediate danger or crisis, encourage contacting local emergency services or a trusted crisis hotline.\nKeep the answer safe, specific, and directly relevant to the user's question without being overly brief."
},
{
'role': 'user',
'content': "Question:\n\nI'm experiencing anxiety about social situations and don't know how to cope."
}
],
'completion': [
{
'role': 'assistant',
'content': "Social anxiety can be a difficult and isolating experience, but there are effective treatments available. Let's work on developing coping mechanisms, such as deep breathing and mindfulness, and exposure therapy to gradually confront your fears. We can also explore ways to improve social skills and build self-confidence."
}
],
'chat_template_kwargs': {'enable_thinking': False}
}
4. Încărcarea modelului de bază Nemotron-3
În continuare, vom încărca tokenizerul și modelul de bază NVIDIA Nemotron-3 Nano 4B BF16 de pe Hugging Face. Stabilim, de asemenea, directorul de ieșire pentru adaptorul LoRA și limităm lungimea secvenței la 1024 tokenuri pentru a menține antrenarea gestionabilă pe un GPU de 24GB.
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL_ID = "nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16"
OUTPUT_DIR = "./nemotron-3-nano-4b-bf16-psychology-qa-lora"
MAX_SEQ_LENGTH = 1024
tokenizer = AutoTokenizer.from_pretrained(
MODEL_ID,
token=hf_token,
trust_remote_code=True,
use_fast=True,
)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
base_model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
token=hf_token,
trust_remote_code=True,
dtype=torch.bfloat16,
device_map="auto",
attn_implementation="eager",
)
base_model.config.use_cache = False
base_model.config.pad_token_id = tokenizer.pad_token_id
base_model.config.eos_token_id = tokenizer.eos_token_id
base_model.generation_config.pad_token_id = tokenizer.pad_token_id
base_model.generation_config.eos_token_id = tokenizer.eos_token_id
base_model.generation_config.use_cache = False
base_model.generation_config.do_sample = False
base_model.generation_config.top_p = None
base_model.generation_config.min_new_tokens = None
base_model.generation_config.repetition_penalty = 1.08
base_model.generation_config.no_repeat_ngram_size = 4
5. Crearea funcțiilor ajutătoare pentru generare
Înainte de ajustarea fină, vom crea câteva funcții utile pentru a testa răspunsurile modelului. Aceste funcții construiesc promptul de chat, generează un răspuns, elimină etichetele de „gândire” nedorite și stochează rezultatele într-un mic tabel de comparație.
import gc
import pandas as pd
from tqdm.auto import tqdm
def clear_cuda_cache():
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
def build_messages(question, system_prompt=SYSTEM_PROMPT):
return [
{"role": "system", "content": system_prompt},
{
"role": "user",
"content": USER_TEMPLATE.format(question=clean_text(question)),
},
]
def remove_thinking_text(text):
text = text.strip()
while "<think>" in text and "</think>" in text:
start = text.find("<think>")
end = text.find("</think>", start) + len("</think>")
text = (text[:start] + text[end:]).strip()
if "</think>" in text:
text = text.split("</think>")[-1].strip()
return text.replace("<think>", "").replace("</think>", "").strip()
def generate_answer(
model, tokenizer, question, system_prompt=SYSTEM_PROMPT, max_new_tokens=180
):
messages = build_messages(question, system_prompt)
device = next(model.parameters()).device
inputs = tokenizer.apply_chat_template(
messages,
tokenize=True,
**CHAT_TEMPLATE_KWARGS,
add_generation_prompt=True,
return_dict=True,
return_tensors="pt",
)
inputs = {key: value.to(device) for key, value in inputs.items()}
input_len = inputs["input_ids"].shape[-1]
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=False,
use_cache=False,
repetition_penalty=1.08,
no_repeat_ngram_size=4,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
)
decoded = tokenizer.decode(outputs[0][input_len:], skip_special_tokens=True).strip()
return remove_thinking_text(decoded)
def generate_sample_table(model, tokenizer, examples, output_column):
rows = []
model.eval()
for ex in tqdm(examples, desc=f"Generating {output_column}", leave=False):
rows.append(
{
"question": clean_text(ex["question"]),
"reference_response_j": clean_text(ex["response_j"]),
output_column: generate_answer(model, tokenizer, ex["question"]),
}
)
return pd.DataFrame(rows)
6. Rulare evaluare pe eșantion înainte de ajustarea fină
Înainte de antrenare, vom genera câteva răspunsuri de la modelul de bază Nemotron-3. Acest lucru ne oferă o bază de referință, astfel încât ulterior să putem compara modul în care răspunde modelul înainte și după ajustarea fină cu LoRA.
Aici selectăm trei exemple din setul de test și generăm răspunsuri folosind funcția ajutătoare creată anterior.
sample_examples = [dataset["test"][idx] for idx in range(min(3, len(dataset["test"])))]
pre_samples = generate_sample_table(
base_model,
tokenizer,
sample_examples,
"base_model_answer"
)
pre_samples
Rezultatul este un mic tabel cu întrebarea originală, răspunsul de referință din response_j și răspunsul generat de modelul de bază. Acest tabel va fi util mai târziu, când îl vom compara cu răspunsurile modelului ajustat fin.

7. Configurarea LoRA și a setărilor de antrenare
Acum vom pregăti modelul pentru ajustare fină cu LoRA. Activăm gradient checkpointing pentru a reduce utilizarea memoriei, apoi creăm o configurație LoRA care vizează toate straturile liniare din model.
from peft import LoraConfig
base_model.gradient_checkpointing_enable()
base_model.config.use_cache = False
lora_config = LoraConfig(
r=32,
lora_alpha=64,
lora_dropout=0.1,
bias="none",
task_type="CAUSAL_LM",
target_modules="all-linear",
)
În continuare, definim setările pentru ajustarea fină supervizată folosind SFTConfig. Aceste setări controlează mărimea batch-ului, rata de învățare, numărul de epoci, frecvența evaluării, strategia de salvare și antrenarea în BF16.
from trl import SFTConfig, SFTTrainer
training_args = SFTConfig(
output_dir=OUTPUT_DIR,
per_device_train_batch_size=8,
per_device_eval_batch_size=8,
gradient_accumulation_steps=8,
learning_rate=5e-5,
weight_decay=0.01,
lr_scheduler_type="linear",
warmup_ratio=0.05,
num_train_epochs=2,
logging_steps=50,
eval_strategy="steps",
eval_steps=50,
save_strategy="steps",
save_steps=100,
save_total_limit=2,
load_best_model_at_end=True,
metric_for_best_model="eval_loss",
greater_is_better=False,
gradient_checkpointing=True,
bf16=True,
fp16=False,
tf32=True,
max_length=MAX_SEQ_LENGTH,
packing=False,
completion_only_loss=True,
remove_unused_columns=False,
dataloader_num_workers=4,
optim="adamw_torch_fused",
report_to="none",
seed=SEED,
)
8. Antrenarea și salvarea adaptorului LoRA
Acum putem crea SFTTrainer, atașa configurația LoRA și porni ajustarea fină. Înainte de antrenare, verificăm și câți parametri sunt antrenabili pentru a confirma că adaptorul LoRA a fost atașat corect.
trainer = SFTTrainer(
model=base_model,
args=training_args,
train_dataset=sft_dataset["train"],
eval_dataset=sft_dataset["validation"],
peft_config=lora_config,
processing_class=tokenizer,
)
trainable_params = sum(
param.numel() for param in trainer.model.parameters() if param.requires_grad
)
all_params = sum(param.numel() for param in trainer.model.parameters())
if trainable_params == 0:
raise RuntimeError(
"No trainable LoRA parameters were attached. Check target_modules before training."
)
print(f"Trainable LoRA parameters: {trainable_params:,}")
print(f"All parameters visible to trainer: {all_params:,}")
print(f"Trainable percentage: {100 * trainable_params / all_params:.4f}%")
train_result = trainer.train()
trainer.model.eval()
trainer.model.config.use_cache = False
trainer.model.generation_config.use_cache = False
train_result
În timpul antrenării, pierderea pe antrenare și pe validare ar trebui să scadă treptat. De obicei, acest lucru înseamnă că modelul învață stilul de răspuns din setul de date.

După antrenare, salvați local adaptorul LoRA și tokenizerul:
trainer.model.save_pretrained(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)
Apoi încărcați adaptorul ajustat fin pe Hugging Face:
HUB_REPO_ID = "kingabzpro/nemotron-3-nano-4b-bf16-psychology-qa-lora"
trainer.model.push_to_hub(HUB_REPO_ID, private=False)
tokenizer.push_to_hub(HUB_REPO_ID, private=False)
Adaptorul ajustat fin este acum salvat local și încărcat pe Hugging Face la HUB_REPO_ID.

Sursă: kingabzpro/nemotron-3-nano-4b-bf16-psychology-qa-lora
9. Compararea răspunsurilor înainte și după ajustarea fină
În final, vom genera răspunsuri de la modelul ajustat fin și le vom compara cu ieșirile modelului de bază. Acest lucru ne ajută să vedem dacă ajustarea fină cu LoRA a îmbunătățit alinierea modelului la răspunsurile de referință.
post_samples = generate_sample_table(
trainer.model,
tokenizer,
sample_examples,
"fine_tuned_answer"
)
comparison = pre_samples[
["question", "reference_response_j", "base_model_answer"]
].merge(
post_samples[["question", "fine_tuned_answer"]],
on="question",
how="left",
)
for idx, row in comparison.iterrows():
print("=" * 100)
print(f"Sample {idx + 1}")
print("=" * 100)
print("\nQUESTION:\n")
print(row["question"])
print("\nREFERENCE RESPONSE_J:\n")
print(row["reference_response_j"])
print("\nBASE MODEL ANSWER:\n")
print(row["base_model_answer"])
print("\nFINE-TUNED ANSWER:\n")
print(row["fine_tuned_answer"])
print("\n")

Modelul ajustat fin a devenit mai aliniat cu stilul răspunsurilor de referință. A fost mai concis și a rămas mai aproape de răspunsurile din setul de date. Totuși, modelul de bază a oferit uneori răspunsuri mai detaliate și practice.
De exemplu, modelul ajustat fin a îmbunătățit alinierea pe întrebările legate de gestionarea stresului și concentrare, dar modelul de bază a dat un răspuns mai puternic pentru exemplul legat de somn, deoarece a inclus mai multe detalii utile.
În ansamblu, modelul ajustat fin este mai bun dacă obiectivul este să potriviți stilul setului de date de referință. Dacă obiectivul este maximul de utilitate, modelul de bază poate performa încă mai bine în unele cazuri, deoarece poate oferi răspunsuri mai calde și mai detaliate.
Dacă întâmpinați probleme la rularea codului de mai sus, consultați notebook-ul din repo-ul Hugging Face: fine-tune-nemotron-nano.ipynb
Concluzii
Chiar și după ajustarea fină a peste 100 de LLM-uri, acest model a necesitat mai multă configurare decât mă așteptam. Provocarea principală a fost dependența mamba_ssm, care se poate strica ușor sau poate intra în conflict cu un mediu Python local existent.
Din acest motiv, recomand folosirea unui mediu curat pentru acest flux de lucru. În cazul meu, cea mai ușoară cale a fost să reconstruiesc mediul, să instalez versiunea corectă de PyTorch, să fixez pachetele legate de Mamba și apoi să rulez notebook-ul de acolo.
O altă limitare este cuantizarea. Pentru această configurație, nu am putut încărca pur și simplu modelul în 4 biți și să îl ajustez fin ca într-un flux QLoRA standard, ca în tutorialul meu despre Qwen3.5 Small. A trebuit să încarc modelul complet BF16 și apoi să îl ajustez fin cu LoRA. Pentru un model de 4B, acest lucru este încă gestionabil pe un GPU de 24GB, dar pentru modele de 12B și peste, utilizarea memoriei poate deveni rapid o problemă.
Cu toate acestea, ajustarea fină pe GPU-uri pentru consumatori a devenit mult mai accesibilă. Cu o placă de 24GB precum RTX 3090, este acum posibil să adaptați modele deschise puternice la un anumit stil sau domeniu fără a avea nevoie de un cluster mare de antrenare.
Per total, Nemotron-3 Nano este un model capabil, dar necesită o configurare atentă a mediului. Odată ce dependențele funcționează, se ajustează fin bine și se poate adapta la un nou stil de răspuns cu un număr relativ mic de exemple.