Track
NVIDIA Nemotron-3 — это новое открытое семейство моделей NVIDIA, созданное для задач рассуждения, кодирования, чатов и агентных AI‑процессов. В него входят модели разного размера — Nano, Super и Ultra, — чтобы разработчики могли выбирать между более компактными и эффективными моделями и более крупными, производительными.
Ключевое обновление в Nemotron-3 — упор на эффективность. Модели спроектированы так, чтобы обеспечивать высокую производительность при более практичном инференсе и дообучении. Версия Nano особенно полезна для практических экспериментов: она может работать на более доступных конфигурациях GPU по сравнению с крупными моделями.
В этом руководстве мы дообучим NVIDIA Nemotron-3-Nano-4B на наборе данных вопросов и ответов по психологии. Мы будем использовать Low-Rank Adaptation (LoRA), Transformers Reinforcement Learning (TRL) и Hugging Face, чтобы подготовить данные, обучить модель, сохранить адаптер, отправить его на Hugging Face и сравнить ответы до и после дообучения.
Чтобы начать искать новейшие открытые модели ИИ, строить AI‑агентов и дообучать LLM, рекомендую записаться на наш скилл‑трек Hugging Face Fundamentals.
1. Настройка окружения
Nemotron-3 Nano использует гибридную архитектуру, поэтому пакеты, связанные с Mamba, нужно установить корректно. В нотбуке Jupyter мы сначала удаляем текущий стек PyTorch и переустанавливаем сборку PyTorch 2.7.1 для CUDA 12.8, совместимую с закреплёнными версиями mamba_ssm и causal_conv1d, используемыми здесь.
Мы также устанавливаем основные библиотеки для дообучения: transformers, trl, accelerate, datasets, peft и 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"
После установки пакетов проверьте, доступна ли CUDA и видит ли PyTorch ваш GPU. Этот ноутбук рассчитан на GPU с 24 ГБ, поэтому он предупредит, если у вашей видеокарты меньше 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
Вывод:
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.
Установите ваш токен Hugging Face как переменную окружения с именем HF_TOKEN. Это позволит ноутбуку скачать модель Nemotron-3, а позже — отправить дообученный адаптер LoRA на 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. Загрузка и обработка набора данных
Далее мы загрузим набор данных вопросов и ответов по психологии с Hugging Face. Набор содержит столбец question и два столбца ответов: response_j и response_k. В этом руководстве мы будем использовать response_j в качестве целевого ответа для контролируемого дообучения.
Сначала мы загружаем набор данных, перемешиваем его для воспроизводимости и создаём обучающую, валидационную и тестовую выборки.
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
Вывод:
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
})
})
Прежде чем форматировать набор данных для обучения, проверьте названия столбцов и просмотрите один пример. Это подтверждает, что набор данных загружен корректно и содержит ожидаемые поля вопроса и ответа.
dataset["train"].column_names, dataset["train"][0]
Вывод:
(
['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. Форматирование набора данных для дообучения в TRL
Теперь мы преобразуем набор данных в формат подсказка–завершение, ожидаемый TRL. Каждый пример будет включать системную подсказку, психологический вопрос пользователя и целевой ответ ассистента из response_j.
Системная подсказка указывает модели, как отвечать: быть поддерживающей, избегать скрытых рассуждений, давать практические рекомендации и не выдавать себя за лицензированного специалиста по психическому здоровью.
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]
Вывод:
{
'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. Загрузка базовой модели Nemotron-3
Теперь мы загрузим токенайзер и базовую модель NVIDIA Nemotron-3 Nano 4B BF16 с Hugging Face. Также зададим каталог для сохранения адаптера LoRA и ограничим длину последовательности до 1024 токенов, чтобы уместиться в пределы 24‑ГБ GPU.
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. Создание вспомогательных функций генерации
Перед дообучением создадим несколько вспомогательных функций для проверки ответов модели. Эти функции формируют чат‑подсказку, генерируют ответ, удаляют нежелательные теги «мышления» и сохраняют результаты в небольшой таблице для сравнения.
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. Предварительная оценка до дообучения
До начала обучения сгенерируем несколько ответов базовой моделью Nemotron-3. Это даст нам базовый уровень, с которым позже можно будет сравнить ответы до и после дообучения LoRA.
Здесь мы выбираем три примера из тестовой выборки и генерируем ответы с помощью созданной ранее вспомогательной функции.
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
Вывод представляет собой небольшую таблицу с исходным вопросом, эталонным ответом из response_j и ответом, сгенерированным базовой моделью. Эта таблица пригодится позже при сравнении с ответами дообученной модели.

7. Настройка LoRA и параметров обучения
Теперь подготовим модель к дообучению LoRA. Мы включим gradient checkpointing для снижения потребления памяти, затем создадим конфигурацию LoRA, нацеливающуюся на все линейные слои модели.
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",
)
Далее зададим параметры контролируемого дообучения с помощью SFTConfig. Эти настройки определяют размер батча, скорость обучения, число эпох, частоту валидации, стратегию сохранения и обучение в 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. Обучение и сохранение адаптера LoRA
Теперь мы можем создать SFTTrainer, подключить конфигурацию LoRA и запустить дообучение. Перед стартом также проверим число обучаемых параметров, чтобы убедиться, что адаптер LoRA подключён корректно.
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
В ходе обучения train loss и validation loss должны постепенно снижаться. Обычно это означает, что модель перенимает стиль ответов из набора данных.

После обучения сохраните адаптер LoRA и токенайзер локально:
trainer.model.save_pretrained(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)
Затем отправьте дообученный адаптер на 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)
Дообученный адаптер теперь сохранён локально и загружен на Hugging Face в репозиторий HUB_REPO_ID.

Источник: kingabzpro/nemotron-3-nano-4b-bf16-psychology-qa-lora
9. Сравнение ответов до и после дообучения
Наконец, мы сгенерируем ответы дообученной моделью и сравним их с выходами базовой модели. Это поможет понять, улучшила ли LoRA‑настройка соответствие модели эталонным ответам.
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")

Дообученная модель стала лучше соответствовать стилю эталонных ответов. Она отвечала более лаконично и ближе к формулировкам из датасета. Однако базовая модель иногда давала более подробные и практичные ответы.
Например, дообученная модель улучшила соответствие на вопросах об управлении стрессом и концентрации, но в примере, связанном со сном, базовая модель оказалась сильнее благодаря более полезным деталям.
В целом дообученная модель предпочтительнее, если ваша цель — соответствовать стилю эталонного набора данных. Если же цель — максимальная полезность, базовая модель в некоторых случаях может быть лучше, так как даёт более тёплые и развёрнутые ответы.
Если у вас возникли сложности с запуском кода выше, обратитесь к ноутбуку в репозитории Hugging Face: fine-tune-nemotron-nano.ipynb
Итоги
Даже после дообучения 100+ LLM эта модель потребовала больше настроек, чем ожидалось. Основной сложностью стала зависимость mamba_ssm, которая легко ломается или конфликтует с существующим локальным окружением Python.
Из-за этого я рекомендую использовать «чистое» окружение для этого пайплайна. В моём случае самым простым путём было пересобрать окружение, установить правильную версию PyTorch, зафиксировать версии пакетов, связанных с Mamba, и уже затем запускать ноутбук.
Ещё одно ограничение — квантование. В этой конфигурации я не смог просто загрузить модель в 4‑битном формате и дообучить её по стандартной схеме QLoRA, как в моём руководстве по Qwen3.5 Small. Пришлось загружать полную BF16‑модель и затем дообучать её с LoRA. Для модели 4B это всё ещё посильно на 24‑ГБ GPU, но для моделей на 12B и выше потребление памяти быстро становится проблемой.
Тем не менее дообучение на потребительских GPU стало гораздо доступнее. С видеокартой на 24 ГБ, такой как RTX 3090, теперь можно адаптировать сильные открытые модели под конкретный стиль или домен без крупного обучающего кластера.
В общем, Nemotron-3 Nano — достойная модель, но ей требуется аккуратная настройка окружения. Когда зависимости работают, она хорошо дообучается и может адаптироваться к новому стилю ответов на относительно небольшом количестве примеров.