Tracks
NVIDIA Nemotron-3 là dòng mô hình mở mới của NVIDIA được xây dựng cho các quy trình lập luận, lập trình, trò chuyện và AI tác tử. Dòng này bao gồm nhiều kích thước mô hình như Nano, Super và Ultra, giúp nhà phát triển có thể chọn giữa mô hình nhỏ, hiệu quả và mô hình lớn, hiệu năng cao.
Cập nhật quan trọng ở Nemotron-3 là tập trung vào hiệu quả. Các mô hình được thiết kế để mang lại hiệu năng tốt trong khi vẫn thực tiễn hơn khi suy luận và tinh chỉnh. Phiên bản Nano đặc biệt hữu ích cho việc thử nghiệm thực hành vì có thể chạy trên các cấu hình GPU phổ biến hơn so với các mô hình lớn.
Trong hướng dẫn này, chúng ta sẽ tinh chỉnh NVIDIA Nemotron-3-Nano-4B trên bộ dữ liệu Hỏi - đáp tâm lý học. Chúng ta sẽ dùng Low-Rank Adaptation (LoRA), Transformers Reinforcement Learning (TRL) và Hugging Face để chuẩn bị dữ liệu, huấn luyện mô hình, lưu adapter, đẩy lên Hugging Face, và so sánh phản hồi trước và sau khi tinh chỉnh.
Để bắt đầu tìm mô hình AI nguồn mở mới nhất, xây dựng tác tử AI và tinh chỉnh LLM, tôi khuyến nghị đăng ký lộ trình kỹ năng Hugging Face Fundamentals của chúng tôi.
1. Thiết lập môi trường
Nemotron-3 Nano dùng kiến trúc lai, vì vậy các gói liên quan đến Mamba cần được cài đặt đúng. Trong Jupyter notebook, đầu tiên chúng ta gỡ bỏ bộ PyTorch hiện tại và cài lại bản PyTorch 2.7.1 build CUDA 12.8, tương thích với phiên bản mamba_ssm và causal_conv1d được ghim trong hướng dẫn này.
Chúng ta cũng cài đặt các thư viện tinh chỉnh cốt lõi, bao gồm transformers, trl, accelerate, datasets, peft và 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"
Sau khi cài đặt gói, hãy kiểm tra CUDA khả dụng và PyTorch có phát hiện GPU của bạn hay không. Notebook này tối ưu cho GPU 24 GB, nên sẽ cảnh báo nếu GPU của bạn có VRAM ít hơn.
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
Kết quả:
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.
Đặt token Hugging Face của bạn làm biến môi trường tên HF_TOKEN. Việc này cho phép notebook tải mô hình Nemotron-3 và sau đó đẩy adapter LoRA đã tinh chỉnh lên 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. Tải và xử lý bộ dữ liệu
Tiếp theo, chúng ta sẽ tải bộ dữ liệu Hỏi - đáp tâm lý học từ Hugging Face. Bộ dữ liệu có cột question và hai cột phản hồi: response_j và response_k. Trong hướng dẫn này, chúng ta sẽ dùng response_j làm câu trả lời mục tiêu cho tinh chỉnh có giám sát.
Trước tiên, chúng ta tải bộ dữ liệu, xáo trộn để có thể tái lập, và tạo các tập train, validation và test.
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
Kết quả:
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
})
})
Trước khi định dạng bộ dữ liệu để huấn luyện, hãy kiểm tra tên cột và xem một ví dụ. Việc này xác nhận bộ dữ liệu đã được tải chính xác và chứa các trường câu hỏi, phản hồi như mong đợi.
dataset["train"].column_names, dataset["train"][0]
Kết quả:
(
['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. Định dạng bộ dữ liệu cho tinh chỉnh TRL
Bây giờ chúng ta sẽ chuyển bộ dữ liệu sang định dạng prompt-completion theo yêu cầu của TRL. Mỗi ví dụ sẽ bao gồm một prompt hệ thống, câu hỏi tâm lý của người dùng và phản hồi mục tiêu của trợ lý từ response_j.
Prompt hệ thống hướng dẫn mô hình cách phản hồi: mang tính hỗ trợ, tránh để lộ dấu vết suy luận ẩn, đưa ra gợi ý thực tế và tránh hành xử như một chuyên gia sức khỏe tâm thần được cấp phép.
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]
Kết quả:
{
'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. Tải mô hình gốc Nemotron-3
Tiếp theo, chúng ta sẽ tải tokenizer và mô hình gốc NVIDIA Nemotron-3 Nano 4B BF16 từ Hugging Face. Chúng ta cũng đặt thư mục đầu ra cho adapter LoRA và giới hạn độ dài chuỗi ở 1024 token để việc huấn luyện có thể quản lý được trên GPU 24 GB.
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. Tạo các hàm trợ giúp sinh phản hồi
Trước khi tinh chỉnh, chúng ta sẽ tạo một vài hàm trợ giúp để kiểm tra phản hồi của mô hình. Các hàm này xây dựng prompt trò chuyện, sinh câu trả lời, loại bỏ các thẻ suy nghĩ không mong muốn và lưu kết quả trong một bảng so sánh nhỏ.
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. Đánh giá mẫu trước khi tinh chỉnh
Trước khi huấn luyện, chúng ta sẽ sinh một vài phản hồi từ mô hình Nemotron-3 gốc. Điều này cung cấp một đường cơ sở để sau đó chúng ta có thể so sánh cách mô hình phản hồi trước và sau khi tinh chỉnh LoRA.
Ở đây, chúng ta chọn ba ví dụ từ tập test và sinh câu trả lời bằng hàm trợ giúp đã tạo trước đó.
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
Kết quả là một bảng nhỏ gồm câu hỏi gốc, câu trả lời tham chiếu từ response_j và câu trả lời do mô hình gốc sinh ra. Bảng này sẽ hữu ích về sau khi chúng ta so sánh với phản hồi của mô hình đã tinh chỉnh.

7. Cấu hình LoRA và thiết lập huấn luyện
Bây giờ chúng ta sẽ chuẩn bị mô hình cho tinh chỉnh LoRA. Bật gradient checkpointing để giảm sử dụng bộ nhớ, sau đó tạo cấu hình LoRA nhắm tới tất cả các lớp tuyến tính trong mô hình.
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",
)
Tiếp theo, chúng ta định nghĩa thiết lập tinh chỉnh có giám sát bằng SFTConfig. Các thiết lập này điều khiển batch size, tốc độ học, số epoch, tần suất đánh giá, chiến lược lưu, và huấn luyệ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. Huấn luyện và lưu adapter LoRA
Giờ chúng ta có thể tạo SFTTrainer, gắn cấu hình LoRA và bắt đầu tinh chỉnh. Trước khi huấn luyện, chúng ta cũng kiểm tra số lượng tham số có thể huấn luyện để xác nhận adapter LoRA đã được gắn đúng.
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
Trong quá trình huấn luyện, loss huấn luyện và loss kiểm định thường sẽ giảm dần. Điều này thường cho thấy mô hình đang học phong cách phản hồi từ bộ dữ liệu.

Sau khi huấn luyện, lưu adapter LoRA và tokenizer tại máy:
trainer.model.save_pretrained(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)
Sau đó đẩy adapter đã tinh chỉnh lên 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)
Adapter đã tinh chỉnh hiện được lưu cục bộ và tải lên Hugging Face dưới HUB_REPO_ID.

Nguồn: kingabzpro/nemotron-3-nano-4b-bf16-psychology-qa-lora
9. So sánh phản hồi trước và sau khi tinh chỉnh
Cuối cùng, chúng ta sẽ sinh câu trả lời từ mô hình đã tinh chỉnh và so sánh với đầu ra của mô hình gốc. Điều này giúp xem liệu tinh chỉnh LoRA có cải thiện mức độ khớp với phản hồi tham chiếu hay không.
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")

Mô hình đã tinh chỉnh trở nên khớp hơn với phong cách phản hồi tham chiếu. Nó súc tích hơn và bám sát câu trả lời từ bộ dữ liệu. Tuy nhiên, đôi khi mô hình gốc đưa ra phản hồi chi tiết và thực tiễn hơn.
Ví dụ, mô hình tinh chỉnh cải thiện độ khớp ở các câu hỏi về quản lý căng thẳng và tập trung, nhưng mô hình gốc cho phản hồi tốt hơn ở ví dụ về giấc ngủ vì cung cấp nhiều chi tiết hữu ích hơn.
Nhìn chung, mô hình đã tinh chỉnh phù hợp hơn nếu mục tiêu của bạn là khớp với phong cách của bộ dữ liệu tham chiếu. Nếu mục tiêu là mức độ hữu ích tối đa, trong một số trường hợp mô hình gốc vẫn có thể hoạt động tốt hơn do đưa ra câu trả lời ấm áp và chi tiết hơn.
Nếu bạn gặp vấn đề khi chạy mã ở trên, hãy tham khảo notebook trong repo Hugging Face: fine-tune-nemotron-nano.ipynb
Tổng kết
Ngay cả sau khi tinh chỉnh hơn 100 LLM, mô hình này vẫn cần nhiều bước thiết lập hơn dự kiến. Thách thức chính là phụ thuộc mamba_ssm, vốn dễ gây lỗi hoặc xung đột với môi trường Python cục bộ hiện có.
Vì vậy, tôi khuyến nghị sử dụng một môi trường sạch cho quy trình này. Trường hợp của tôi, cách dễ nhất là dựng lại môi trường, cài đúng phiên bản PyTorch, ghim các gói liên quan đến Mamba, rồi chạy notebook từ đó.
Một hạn chế khác là lượng tử hóa. Với thiết lập này, tôi không thể chỉ tải mô hình ở 4-bit và tinh chỉnh như quy trình QLoRA tiêu chuẩn, như trong hướng dẫn Qwen3.5 Small của tôi. Tôi phải tải toàn bộ mô hình BF16 rồi tinh chỉnh bằng LoRA. Với mô hình 4B, điều này vẫn quản lý được trên GPU 24 GB, nhưng với mô hình 12B trở lên, mức sử dụng bộ nhớ có thể nhanh chóng thành vấn đề.
Dẫu vậy, tinh chỉnh trên GPU cá nhân đã trở nên dễ tiếp cận hơn nhiều. Với card 24 GB như RTX 3090, giờ đây có thể thích ứng các mô hình mở mạnh mẽ theo một phong cách hoặc miền cụ thể mà không cần cụm huấn luyện lớn.
Tổng thể, Nemotron-3 Nano là một mô hình có năng lực, nhưng cần thiết lập môi trường cẩn thận. Khi các phụ thuộc đã ổn định, mô hình tinh chỉnh tốt và có thể thích ứng với phong cách phản hồi mới chỉ với số lượng ví dụ tương đối nhỏ.

Là một nhà khoa học dữ liệu được chứng nhận, tôi đam mê tận dụng công nghệ tiên tiến để tạo ra các ứng dụng học máy đổi mới. Với nền tảng vững chắc về nhận dạng giọng nói, phân tích và báo cáo dữ liệu, MLOps, AI hội thoại và NLP, tôi đã rèn giũa kỹ năng phát triển các hệ thống thông minh có thể tạo ra tác động thực sự. Bên cạnh chuyên môn kỹ thuật, tôi cũng là một người truyền đạt tốt, có khả năng chắt lọc các khái niệm phức tạp thành ngôn ngữ rõ ràng, súc tích. Nhờ đó, tôi trở thành một blogger được nhiều người quan tâm trong lĩnh vực khoa học dữ liệu, chia sẻ góc nhìn và kinh nghiệm với cộng đồng các chuyên gia dữ liệu ngày càng lớn. Hiện tại, tôi tập trung vào sáng tạo và biên tập nội dung, làm việc với các mô hình ngôn ngữ lớn để phát triển nội dung mạnh mẽ và hấp dẫn, giúp doanh nghiệp và cá nhân tận dụng tối đa dữ liệu của mình.