I tried fine-tuning Nemotron 3 Nano in Japanese on DGX Spark

I tried fine-tuning Nemotron 3 Nano in Japanese on DGX Spark

We fine-tuned Nemotron 3 Nano on DGX Spark using QLoRA with Japanese persona data and summarized the results of both quantitative and qualitative evaluation. With 1,000 training samples, we confirmed reductions in English code-switching and improvements in business document quality, while also uncovering ecosystem-level challenges such as bugs in the inference implementation and pitfalls in LoRA integration.
2026.02.16

This page has been translated by machine translation. View original

Introduction

Hello, I'm Morishige from Classmethod's Manufacturing Business Technology Department.

In my previous article "A Look at the State of Local LLMs in 2026," I wrote that Nemotron 3 Nano had "no Japanese benchmark results," but after trying it on a DGX Spark, I found it was quite excellent even in its raw state. That got me curious, and I decided to try fine-tuning it for Japanese using the NVIDIA-published synthetic persona dataset Nemotron-Personas-Japan.

To be upfront about this: I naively assumed that training on persona dialogue data would improve Japanese benchmark scores, but the capabilities measured by the training data (descriptions of Japanese people's lives and occupations) and the evaluation benchmarks (multiple-choice general knowledge questions) are quite different. I'll go into more detail about this mismatch in the latter half of the article, but since things didn't completely fall apart, I'm sharing the process including the trial and error.

This article summarizes the results of QLoRA fine-tuning Nemotron 3 Nano on Japanese data on a DGX Spark and comparing performance before and after, both quantitatively and qualitatively.

What is Nemotron 3 Nano?

Nemotron 3 Nano is a MoE (Mixture of Experts) model released by NVIDIA in December 2025. Of its 31.6B total parameters, only 3.6B are actually active at any time.

The architecture is interesting: of 52 layers, approximately 92% are Mamba-2 State Space Models and the remaining 8% are Transformer self-attention mechanisms, making it a hybrid architecture (nemotron_h architecture). Mamba-2 can process long contexts with constant computation, enabling support for context lengths of up to 1 million tokens. Each layer has 128 routed experts, with only 6 activated per token.

It also works well with DGX Spark. In a unified memory environment, MoE models don't incur PCIe transfer penalties, so expert switching runs smoothly. At Q4 quantization it uses about 24GB, roughly 1/5 of the 128GB memory, leaving plenty of room for fine-tuning.

The license is the NVIDIA Open Model License, which permits commercial use, modification, and redistribution.

Baseline Evaluation

First, let's confirm the Japanese language performance of the raw model.

Measuring Commonsense Reasoning with JCommonsenseQA

I evaluated all 1,119 questions from the JCommonsenseQA v1.3 validation set on DGX Spark using 3-shot prompting. Here is a comparison with other models.

Model Active Parameters Accuracy Correct
Gemma 3 27B 27B (Dense) 93.9% 1051/1119
gpt-oss:20b 3.6B (MoE) 92.7% 1037/1119
Nemotron 3 Nano 3.6B (MoE) 92.5% 1035/1119
Gemma 3 12B 12B (Dense) 91.8% 1027/1119
Qwen2.5-Coder 32B 32B (Dense) 90.1% 1008/1119
GLM-4.7-Flash 3B (MoE) 81.9% 917/1119

Achieving 92.5% with only 3.6B active parameters is a score that rivals the 27B Dense Gemma 3. Even in its raw state, it seems sufficiently practical for Japanese commonsense reasoning.

Verification Environment

Hardware

Item Value
Device NVIDIA DGX Spark
GPU GB10 Grace Blackwell Superchip
Memory 128GB Unified Memory (LPDDR5x)
CPU Cortex-X925 x10 + Cortex-A725 x10 (20 cores)
OS Ubuntu 24.04.3 LTS (aarch64)
CUDA 13.0

Software

Using NVIDIA's official PyTorch container as the base, I installed the necessary libraries for fine-tuning.

Item Version
Container nvcr.io/nvidia/pytorch:25.11-py3
Python 3.12.3
PyTorch 2.10.0 (NVIDIA build)
transformers 5.1.0
PEFT 0.18.1
TRL 0.28.0
bitsandbytes 0.49.1

Since Unsloth doesn't support the nemotron_h architecture, I used HuggingFace PEFT + TRL directly.

Preparing for Fine-Tuning

Setting Up the Docker Environment

Since DGX Spark is an aarch64 environment, packages that only have x86_64 wheels can be a struggle. Using NVIDIA's official PyTorch container helps avoid CUDA compatibility issues.

# Pull the NVIDIA PyTorch container
docker pull nvcr.io/nvidia/pytorch:25.11-py3

# Create working directory
mkdir -p ~/nemotron-ft && cd ~/nemotron-ft

# Start container (GPU access + working directory mount)
docker run -it --gpus all \
  -v $(pwd):/workspace/nemotron-ft \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  --shm-size=16g \
  --name nemotron-ft \
  nvcr.io/nvidia/pytorch:25.11-py3

Install the fine-tuning libraries inside the container.

pip install peft trl bitsandbytes datasets hf_transfer accelerate

The NVIDIA PyTorch container on DGX Spark already has PyTorch optimized for aarch64 installed, so fewer additional packages need to be installed.

Exploring the Nemotron-Personas-Japan Dataset

Nemotron-Personas-Japan, published by NVIDIA, is a synthetic persona dataset based on Japan's national census and labor statistics.

Item Details
Records 1 million
Personas 6 million (1 record x 6 personas)
Tokens Approximately 1.4 billion
Proper nouns Approximately 950,000
Occupation categories Over 1,500
License CC BY 4.0

Generated with NeMo Data Designer, it contains diverse personas reflecting Japan's demographics including age, gender, region, occupation, and education level. It is entirely synthetic data for privacy protection and contains no PII (personally identifiable information).

Let's first take a look at the data contents.

from datasets import load_dataset

ds = load_dataset("nvidia/Nemotron-Personas-Japan", split="train")
print(f"Total records: {len(ds):,}")  # 1,000,000
print(f"Columns: {ds.column_names}")

Each record has 22 columns, consisting of 6 types of persona fields (professional_persona, sports_persona, arts_persona, travel_persona, culinary_persona, persona) and metadata such as age, occupation, and prefecture. Each persona field contains Japanese text describing a person's background, such as their career history or attachment to local food culture.

Converting to SFT Format

We convert the persona data into instruction-following format QA pairs.

def make_professional_prompt(example):
    """Generate career-related QA pairs"""
    loc = example.get("prefecture", "Japan")
    occ = example.get("occupation", "office worker")
    user_msg = (
        f"I'd like to ask someone working as a {occ} in {loc}. "
        f"Could you tell me about your career and work experience?"
    )
    return user_msg, example["professional_persona"]

I prepared 4 conversion templates and applied them in round-robin to a subset of 1,000 records.

Here is one converted sample.

[USER]
I'd like to ask someone working as a mid-level construction worker in Saga Prefecture.
Could you tell me about your career and work experience?

[ASSISTANT]
Sosuke Ohata is a mid-level manager who prioritizes on-site safety and process transparency,
combining simple CAD design with mobile log data visualization
to encourage his team toward planned and reliable execution...

A characteristic of persona data is that responses are written in the third person. First person would be more natural for SFT data, but since the goal this time was to verify "how effective a small dataset of 1,000 items can be," I kept the conversion logic simple.

Executing Fine-Tuning

Loading the Model and Configuring LoRA

We load Nemotron 3 Nano with 4-bit (NF4) quantization using HuggingFace PEFT and add a LoRA adapter.

from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
)

model = AutoModelForCausalLM.from_pretrained(
    "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16",
    quantization_config=bnb_config,
    device_map={"": 0},
    torch_dtype=torch.bfloat16,
    trust_remote_code=True,
)

The key point this time is the LoRA target modules. Since Nemotron 3 Nano is a hybrid of Transformer and Mamba-2 layers, we target the projection layers of both.

lora_config = LoraConfig(
    r=16,
    lora_alpha=16,
    target_modules=[
        # Transformer self-attention
        "q_proj", "k_proj", "v_proj", "o_proj",
        # MoE feed-forward
        "gate_proj", "up_proj", "down_proj",
        # Mamba-2 projections
        "in_proj", "out_proj",
    ],
    lora_dropout=0,
    bias="none",
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, lora_config)

Let's check the LoRA adapter settings.

Item Value
Rank (r) 16
Alpha 16
Number of target modules 9 (Transformer 7 + Mamba 2)
Trainable parameters 441,936,896 (1.38% of total)
Total parameters 32,019,874,240

Only 1.38% of the total parameters are trained, but the key innovation this time was including not just Transformer layers but also Mamba-2 layers' in_proj and out_proj. Since Mamba layers account for 48 of the 52 total layers, ignoring them would significantly limit the scope of learning.

Running the Training

We run 1 epoch of training using TRL's SFTTrainer.

from trl import SFTConfig, SFTTrainer

sft_config = SFTConfig(
    output_dir="./outputs/nemotron-ft-1k",
    per_device_train_batch_size=2,
    gradient_accumulation_steps=4,
    warmup_steps=10,
    num_train_epochs=1,
    learning_rate=2e-4,
    bf16=True,
    logging_steps=10,
    save_steps=100,
    optim="adamw_8bit",
    max_length=2048,
    packing=False,
)

trainer = SFTTrainer(
    model=model,
    processing_class=tokenizer,
    train_dataset=dataset,
    args=sft_config,
)

trainer.train()

Training Results

Item Value
Data size 1,000
Epochs 1
Total steps 120 (effective batch size 8)
Initial loss 15.78
Final loss 4.72
Training time Approximately 81 minutes
LoRA adapter size 886MB

Looking at the loss curve, it dropped sharply in the first 30 steps (15.78 → 6.65) and then gradually converged — a typical curve. This shows that even with a small dataset of 1,000 items, the model is absorbing Japanese language patterns.

Evaluation Pipeline

Converting to GGUF and Loading into Ollama

One thing to consider here is the inference method.

Using HuggingFace's model.generate() is the simplest approach, but the HuggingFace implementation of Nemotron 3 Nano (modeling_nemotron_h.py) has several bugs in the forward pass, making it impossible to get correct output.

So I switched to reusing Ollama (ggml backend) that I had used for the baseline evaluation. llama.cpp natively supports NemotronHForCausalLM, and this implementation works stably.

To use the LoRA adapter with Ollama, convert it to GGUF format using the following steps.

# Clone the llama.cpp repository
git clone --depth 1 https://github.com/ggml-org/llama.cpp /tmp/llama-cpp

# Prepare Python environment for conversion
uv venv /tmp/gguf-env
source /tmp/gguf-env/bin/activate
uv pip install numpy sentencepiece gguf safetensors protobuf \
  transformers torch --index-url https://download.pytorch.org/whl/cpu

# Convert LoRA adapter to GGUF
python /tmp/llama-cpp/convert_lora_to_gguf.py \
  ~/nemotron-ft/outputs/nemotron-ft-1k/lora/ \
  --outfile /tmp/nemotron-ft-1k-lora.gguf \
  --outtype f32

The converted LoRA GGUF file was 1.77GB. It contains 324 tensors (LoRA A/B pairs for each module).

Next, we create an inference model in Ollama that combines the base model + LoRA adapter.

Modelfile
FROM nemotron-3-nano:latest
ADAPTER /tmp/nemotron-ft-1k-lora.gguf
PARAMETER num_ctx 4096
PARAMETER temperature 0
ollama create nemotron-ft-1k -f Modelfile

Post-Fine-Tuning Evaluation

Before/After Comparison on JCommonsenseQA

I evaluated the fine-tuned model under the same conditions as the baseline (3-shot, all 1,119 questions).

Model Accuracy Correct Difference
Nemotron 3 Nano (base) 92.5% 1035/1119 -
Nemotron 3 Nano (after FT) 93.6% 1047/1119 +1.1% (+12 questions)

Even with QLoRA using just 1,000 persona data items, we saw a +1.1% improvement.

Comparison with Other Models

Let's line it up against the major models introduced in the local LLM article.

Model Active Parameters JCommonsenseQA Notes
Gemma 3 27B 27B 93.9% Dense, 140 languages
Nemotron 3 Nano (after FT) 3.6B 93.6% MoE, Japanese FT
gpt-oss:20b 3.6B 92.7% MoE, OpenAI's first OSS
Nemotron 3 Nano (base) 3.6B 92.5% MoE, Mamba-2 hybrid
Gemma 3 12B 12B 91.8% Dense, Japanese fine-tuned version available
Qwen2.5-Coder 32B 32B 90.1% Dense, code-specialized
GLM-4.7-Flash 3B 81.9% MoE, MIT license

The post-FT score of 93.6% is only 0.3% behind Gemma 3 27B (93.9%), a 27B Dense model. It's interesting how close it gets with only 3.6B active parameters. However, as I'll explain below, the compatibility between this benchmark and the training data isn't great, so it's more appropriate to view the score as confirming "FT didn't break anything" rather than placing too much weight on the number itself.

Observing Japanese Language Changes Through Qualitative Evaluation

Since numbers alone don't tell the whole story, I compared outputs by feeding the same prompts to the models before and after FT.

Natural Japanese

Prompt: "Please suggest a travel plan from Tokyo to Osaka"

The pre-FT (BASE) response opens with "Below, we'll use two representative transportation options, 'Shinkansen' and 'highway bus,' as the basis for..." in a somewhat stiff style. Post-FT, it begins with "I'll introduce some routes and highlights based on your travel purpose and how much time you have," which is a more natural introduction. The transportation comparison table in the post-FT version also had clearer "pros/cons" contrasts and felt easier to read.

Honorifics and Business Documents

Prompt: "Please write an email to your boss requesting a schedule change for next week's meeting"

This is where a difference emerged. In the pre-FT version, English text such as scheduled(スケジュール) appears mixed into the body of the message. Post-FT, everything is unified in Japanese, and it outputs a complete business email format including a signature block (phone number, email address, address). It appears that Japanese business customs contained in the persona data are being reflected.

Japanese Cultural Knowledge

Prompt: "Please explain how Japanese people spend New Year's to a foreign friend"

Both pre-FT and post-FT covered the major New Year's events (year-end cleaning, kagami mochi, hatsumode, etc.), but the style of explanation differs. Pre-FT presents a large table listing "what to do" and "why it's important." Post-FT explains in chronological order: "year-end cleaning → preparing kagami mochi → osechi cuisine," making it easier to follow for foreigners. However, the post-FT output incorrectly refers to New Year's as "Seollal" (Korea's Lunar New Year), revealing the limits of the knowledge acquired through persona data.

Overall, post-FT outputs tended to be more natural in Japanese, with less English mixed in and outputs more aligned with Japanese business customs. Personally, this degree of change from just 1,000 persona data items exceeded my expectations.

Compatibility Between Training Data and Benchmarks

As I mentioned at the beginning, I started this assuming that training on persona dialogue data would improve Japanese benchmarks across the board. In reality, this expectation was overly optimistic — JCommonsenseQA is not a particularly appropriate benchmark for measuring the effects of this fine-tuning.

The training data consists of texts like "career description of a mid-level construction manager in Saga Prefecture" and "persona related to food culture in Shizuoka Prefecture." JCommonsenseQA, on the other hand, consists of multiple-choice questions on general knowledge such as "What do you call the supreme commander of an occupied territory? → Governor-General" and "What are rare earth elements called? → Lanthanides" — quite different domains of knowledge. Ideally, the evaluation metric should be decided first, aligned with the training data, before getting started.

The +1.1% improvement is likely not because the persona data directly reinforced commonsense reasoning, but rather an indirect effect of additional training on Japanese text broadening reading comprehension and vocabulary coverage. Since a difference of +12 questions out of 1,119 is borderline in terms of statistical significance, it seems best to avoid placing too much emphasis on the score.

The changes visible in qualitative evaluation — "English mixed into business emails disappeared" and "introductory sentences became more natural" — are a more direct reflection of the FT effects from persona data. It's more appropriate to view JCommonsenseQA as a confirmation that "FT did not destroy commonsense reasoning ability."

Pitfalls Encountered

There were several unexpected issues during this verification. I'm summarizing them here in the hope that they will be useful for others trying the same thing.

Unsloth Does Not Support Nemotron 3 Nano

While DGX Spark Playbooks introduces QLoRA procedures using Unsloth, Nemotron 3 Nano's nemotron_h architecture (Mamba-2 + Transformer hybrid MoE) is not included in Unsloth's list of supported models. Although FalconH1, a similar hybrid model, is supported by Unsloth, the internal layer structure is different so it couldn't be reused.

I ended up switching to using HuggingFace PEFT + TRL directly, which isn't much more code. It's a shame that Unsloth's optimizations (improved memory efficiency and training speed) aren't available, but with the 128GB unified memory of DGX Spark, there were no memory issues even with plain HuggingFace.

There Are Bugs in the HuggingFace Nemotron 3 Nano Inference Implementation

This was the most challenging part of the entire process. Training completes normally with transformers' Trainer, but calling model.generate() during inference produces corrupted output.

Tracing the cause, I found multiple bugs in HuggingFace's modeling_nemotron_h.py (the file implementing the forward pass for Nemotron 3 Nano). There are issues with the state management of Mamba-2 layers and the routing logic of MoE, and even a single forward pass doesn't yield correct logits.

Training itself works fine since the loss is decreasing, but inference is broken, causing a stumble at the very basic step of "checking the output of the trained model." I initially didn't realize this and wasted a lot of time reviewing the LoRA configuration and dataset over and over.

Weights Get Corrupted with merge_and_unload()

Since inference with HuggingFace wasn't working, I thought of converting to GGUF and evaluating with Ollama, but there was another trap here.

The typical LoRA GGUF conversion flow is "merge adapter into base model with merge_and_unload() → convert to GGUF," but with 4-bit QLoRA, merge_and_unload() causes significant precision degradation during the NF4 → BF16 dequantization process, making the weights unusable. The conversion completes normally, but the output becomes a stream of commas and unrelated words.

The solution is to individually convert the LoRA adapter to GGUF using llama.cpp's convert_lora_to_gguf.py and apply it to the base model using Ollama's ADAPTER directive. This approach doesn't touch the base model's weights at all, so no quantization-induced precision degradation occurs.

It took me a full day to get here...

Summary

By leveraging the 128GB unified memory of DGX Spark, I was able to complete Japanese fine-tuning of Nemotron 3 Nano (31.6B MoE) entirely on a local machine.

Here is a summary of this verification's results.

Item Value
Before fine-tuning JCommonsenseQA 92.5%
After fine-tuning JCommonsenseQA 93.6%
Improvement +1.1% (+12 questions)
Training data 1,000 items (persona QA)
Training time Approximately 81 minutes

With just 1,000 persona data items and QLoRA, the qualitative evaluation clearly showed reductions in English mixing and improvements in business document quality. JCommonsenseQA also showed a +1.1% improvement, but I believe this is an indirect effect of improved Japanese language processing capability rather than a direct effect of the persona data.

On the other hand, the Nemotron 3 Nano ecosystem is still maturing. With Unsloth incompatibility, bugs in the HuggingFace inference implementation, and the merge_and_unload() trap, it was a situation of "training works but building an evaluation pipeline is a struggle." Since the llama.cpp (Ollama) implementation is stable, performing evaluation and inference via GGUF seems like the practical solution for now.

Since this much change was visible with 1,000 items, I'm curious what would happen with training on the full dataset of 1 million items. Also, as a lesson learned from this time, if I'm evaluating conversation quality and natural Japanese that persona data helps develop, I should have incorporated a dialogue quality evaluation like Japanese MT-Bench from the start, rather than a knowledge benchmark like JCommonsenseQA — even though it costs more...

I hope this is useful for those who want to try the same thing on DGX Spark.


国内企業 AI活用実態調査2026 配布中

クラスメソッドが独自に行なったAI診断調査をもとに、企業のAI活用の現在地を調査レポートとしてまとめました。企業規模別の活用度傾向に加え、規模を超えてAI活用を進める企業に共通する取り組みまで、自社の現在地を捉えるためのヒントにぜひ。

国内企業 AI活用実態調査2026

無料でダウンロードする

Share this article