Tao Finetune Huggingface Model
NVIDIAOfficialCreated by NVIDIA Corporation
- Skill
- Workflow
- Software development
- Apache-2.0
- Medium risk
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured, six-step workflow for fine-tuning HuggingFace CV, VLM, and LLM models on local NVIDIA GPUs. It leverages NGC PyTorch containers to ensure environment consistency and reproducibility. The workflow includes automated preflight checks, live research of model-specific recipes, smoke testing on real data, and optional integration with HuggingFace Hub for model sharing. It is designed to be the fallback for models not covered by dedicated TAO skills, ensuring a rigorous, documented path from inspection to deployment.
What it does
- Inspects and qualifies models and datasets for compatibility.
- Audits local hardware and selects the appropriate NGC PyTorch container.
- Performs live research to fetch model-specific training recipes and configurations.
- Generates a complete, reproducible project structure including training and evaluation scripts.
- Executes a mandatory smoke test on real data before full-scale training.
- Pushes fine-tuned models to the HuggingFace Hub and emits a self-contained rerun skill.
When to use it
- When you need to fine-tune a HuggingFace model and no dedicated TAO skill exists for it.
- When you require a reproducible, containerized training pipeline on local NVIDIA hardware.
- When you need to smoke-test a model locally before scaling to larger infrastructure.
- When you want to automate the generation of model cards and training reports.
Advantages
- Ensures environment consistency using NVIDIA-optimized NGC containers.
- Prevents common training failures through mandatory smoke testing and preflight checks.
- Reduces manual configuration by automating script generation and recipe research.
- Provides a clear, auditable path from initial inspection to final model deployment.
Usage examples
- “Fine-tune google/vit-base-patch16-224 on my local image dataset.”
- The agent will inspect the model and dataset, verify hardware, generate the training project, run a smoke test, and initiate the fine-tuning process.
- “Train a Llama-3-8B model using LoRA on my local GPU.”
- The agent will research the appropriate LoRA configuration, set up the NGC environment, and execute the training pipeline with the specified parameters.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/nvidia/tao-finetune-huggingface-model/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip tao-finetune-huggingface-model.zip -d .agents/skills
Documentation by NVIDIA Corporation
<!-- Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. Licensed under the Apache License, Version 2.0; see http://www.apache.org/licenses/LICENSE-2.0 -->
tao-finetune-huggingface-model
Standalone install? If this session was not initialized by the TAO skill bank plugin, run the
tao-setupskill first (host preflight, credentials, cross-skill discovery).
Local NVIDIA GPU fine-tuning for HuggingFace models, grounded in live-fetched documentation with curated references as a fallback safety net. One NGC container, a few focused scripts, one push to HF Hub. Follow the rules in this file; don't improvise.
Dedicated-model routing gate
Before Step 1 or any probe, image selection, package install, venv creation, or
training-code generation, resolve model_id against the packaged model-owner
registry. Use the absolute skill-bank root from which this file was loaded:
python <bank-root>/scripts/resolve_tao_model.py \
--skill-bank <bank-root> \
--model "$MODEL_ID" \
--format jsonThe resolver matches model metadata, including huggingface_model_ids,
network_arch, skill names, and legacy aliases. Routing is internal: a model ID
and task are enough. Never require prompt boilerplate about skills, containers,
or checkpoint formats.
Exit
0: stop this workflow and follow the owning model skill's environment, action metadata, preflight, and checkpoint preparation.Exit
3: no packaged model skill owns the ID. This is the only result that permits Step 1 of the generic workflow.Any other nonzero exit: ownership discovery is broken or ambiguous. Stop and resolve that error; do not silently fall back to generic Hugging Face training.
Hugging Face hosting never overrides ownership. Do not use this workflow to
bypass a matched skill or ask the user to prescribe its internal preparation.
For example, nvidia/Cosmos3-Nano routes to tao-finetune-cosmos-reason.
Do not create a host training venv in this workflow. Its default execution path is the NGC container documented below; any venv-based training path requires an explicit user request.
Order of authority (highest first):
User input — explicit
model_id,dataset_id,training_method,config.yamloverrides.Live research — model card, HF repo example, author finetune script, HF task docs, paper; always fetched (Step 3 +
references/research-priorities.md).Curated references (
references/*.md) — fallback when live research is silent/ambiguous.Your training-data memory — last resort; suspect, cross-check against (2)/(3).
Conflict resolution between (2) and (3) and the source-line discrepancy note are
in references/research-priorities.md.
Inputs
Required:
model_id— HuggingFace model ID, e.g.google/vit-base-patch16-224
Conditional credentials (read from the session environment — exported before launching or sourced from a user-approved env file):
HF_TOKEN— only when the model/dataset is gated (read) orpush_to_hubis on (write); public + public +push_to_hub: falseneeds none. Value never read — presence-only via[ -n "$HF_TOKEN" ].WANDB_API_KEY,WANDB_PROJECT— only when WandB is enabled;WANDB_MODE=disabledopts out.
Dataset — exactly one:
dataset_id— HuggingFace dataset ID (source:hf)local_dataset_path— local folder or file (source:local); optionallocal_dataset_format∈ {auto, imagefolder, coco, voc, jsonl, arrow, parquet, csv} (default: auto-detect).(omit) — agent recommends popular datasets (source:
recommend)
Optional (have defaults):
task_type— auto-detected from config + model cardn_train=10000,n_eval=1000,n_epochs=3,lora_r=16output_dir=./output/<model_short_name>hf_model_repo— push target; if unset and HF_TOKEN has write access, auto-derived as<whoami>/<model_short_name>-finetuned.push_to_hub=True— set toFalseto skipskip_baseline=False— skip zero-shot baseline eval
Optional deliverables (off by default):
emit_progress_log: false # output_dir/PROGRESS.md (per-step journal)
emit_report: false # reports/report.{pdf,html} with curves & samples
emit_unit_tests: false # tests/ with fake-data heterogeneous-batch testsAll values live in output_dir/config.yaml. Never hardcode in Python.
Execution platform
This skill orchestrates what to run; the platform skills own how to run it on a GPU host — read them first.
| Concern | Authoritative skill |
|---|---|
| GPU host runtime (driver 580, CUDA Toolkit 13.0, NVIDIA Container Toolkit 1.19.0) | tao-skill-bank:tao-setup-nvidia-gpu-host |
docker run flags, NGC auth, mounts, env passthrough, local/remote Docker job preflight (daemon, GPU smoke) | tao-skill-bank:tao-run-on-docker |
Default platform: local-docker — build a one-off image (run-<short>:latest)
and run it on the local Docker daemon. Ask only when the user explicitly needs a
different backend (Brev remote GPU, SLURM/Kubernetes); then run that platform's
Preflight first and route the Steps 4–5 docker run commands through it. The
GPU-runtime and presence-only credential preflights (values never read), the
canonical docker run flag set, discovery of the execution platforms from the
installed platform skills (tao-run-on-docker / -slurm / -kubernetes / -brev, plus
any external one; on a runtime that surfaces only the core router skills, read
skills/platform/tao-run-on-*/SKILL.md frontmatter), and
the workflow-specific flags (--entrypoint /bin/bash -lc, PYTORCH_CUDA_ALLOC_CONF,
--name hft_train) are in references/workflow-intake-preflight.md.
References — fallback safety net
Consulted only when live research is silent, ambiguous, or unavailable; live
docs always win for the specific model and current API. Each step links the
references it needs; full catalog in references/detailed-workflow.md.
Always-on: core-rules.md, error-playbook.md, compat-workarounds.md,
model-discovery.md, dataset-recommendations.md, dataset-sources.md,
dataset-patterns.md, hardware-container.md, research-priorities.md,
cv-scripts.md, vlm-scripts.md, docker-runs.md, hub-push.md,
pipeline-skill-template.md, deliverables.md. Opt-in (when their flag/need
applies): progress-tracking.md, testing.md, reporting.md,
workflow-intake-preflight.md, workflow-generate-train.md, workflow-push-rerun.md.
Rule: before falling back, log the live source you tried and why it was
insufficient (config.yaml notes:, and PROGRESS.md if enabled). [FETCH LIVE]
markers in cv-scripts.md / vlm-scripts.md are a research checklist, not code to
inline — refetch the listed URL if a block has no Step 3 finding.
Core rules
Non-negotiable behaviors. Short version (full enumeration —
hallucinated-imports list, never-without-approval list, full error-recovery and
hardware-sizing tables — in references/core-rules.md, consult before any
training-time decision):
Your HF-library knowledge is outdated. Fetch live docs (model card, HF repo example, task doc) before writing any ML code — don't generate trainer args / collator / transforms from memory (Step 3).
Smoke-test on real data with
--max_steps 1before any full run; no batch launches without a verified smoke.Never silently substitute model_id, dataset_id, or training_method — if what the user asked for doesn't load, stop and ask.
Error recovery is minimal-change. OOM → halve batch, double grad_accum, enable gradient checkpointing (no LoRA switch without approval); NaN → reduce LR 10×; flat loss → inspect collator; same error 3× → stop and ask. Don't loop.
Dataset columns verified BEFORE the collator — rename in
prepare_data.py; restructuring needed → stop and ask.Hardware-sizing thumb (bf16): ≤3B → 24 GB, 7–13B → 80 GB, 30B+ → multi-GPU or LoRA on 1× 80 GB, 70B+ → 8× 80 GB or LoRA. Full finetune won't fit and no LoRA requested → ask before switching.
Workflow — 6 steps
Single pass, sequential; each step has a clear gate before the next begins.
Step 1 — Inspect & qualify
Goal: decide whether to proceed. Probe model + dataset, apply accept/reject,
register applicable compat fixes, write the initial config.yaml.
Prerequisites: MODEL_ID, optional DATASET_ID / local_dataset_path,
optional HF_TOKEN, OUTPUT_DIR (default ./output/<model_short_name>). Probes
run in a CPU-only python:3.12-slim Docker container (bind-mounted .probe/
scratch) so the host needs no virtualenv — Docker must exist first. Docker-presence
guard, container env, full probe invocation, and the model/dataset probe scripts
are in references/workflow-intake-preflight.md, references/model-discovery.md,
and references/dataset-sources.md.
Probe requirements:
Model: load
AutoConfig, read model-card tags, detect task fromarchitectures+ tags + card examples (fallback logging inmodel-discovery.md).Dataset: for recommended datasets, first present 3-5 choices from
dataset-recommendations.md; for local data, bind-mount read-only and usedataset-sources.mdformat detection.Reject early if the model config fails, the task is out of scope, no recipe source exists, or the dataset cannot load / match the task schema.
Evaluate
compat-workarounds.mdagainst the model/task; defer hardware-dependent rules to Step 2.
Write the initial config.yaml (model_id, task, dataset_id or
local_dataset_path, research_sources: [] filled in Step 3,
applicable_workarounds: from Step 1, notes: [] for reference fallbacks,
push_to_hub: true default — annotated template in
references/workflow-intake-preflight.md). Optionally rm -rf "$OUTPUT_DIR/.probe"
once the gate is met.
Gate: config.yaml exists with model, dataset, task, applicable_workarounds;
do not proceed if any field is missing.
Step 2 — Hardware audit & NGC image
Goal: verify Docker + GPU + disk, pick the NGC PyTorch image live, finalize hardware-dependent compat rules.
2a. Audit (hard gate) — three checks (commands in
references/workflow-intake-preflight.md):
GPU host runtime —
tao-setup-nvidia-gpu-host'ssetup-nvidia-gpu-host.sh --backend docker --check-only; on fail, ask approval then re-run with--install --yes.Free-disk soft-warn — override via
MIN_DISK_GB(default 100 GB); recommend ≥ 100 GB for NGC base (~20 GB) + HF cache + checkpoints + data.Conditional credential presence (values never read) —
HF_TOKENonly when gated orpush_to_hubis on;WANDB_*only when WandB is on.
Do not proceed to Step 4 on a hard-fail — Step 4's docker build pulls a
20+ GB NGC base, and a missing nvidia-container-toolkit only surfaces later as
could not select device driver "" with capabilities: [[gpu]]. Record gpu_count,
gpu_name, driver_major, vram_gb_per_gpu in config.yaml.
2b. Pick NGC image (live): from the NVIDIA Deep Learning Frameworks support
matrix (https://docs.nvidia.com/deeplearning/frameworks/support-matrix/index.html),
PyTorch NGC container section, pick the highest-versioned image where
Min driver ≤ detected driver_major and container CUDA ≤ host CUDA Toolkit
(match closely so cuDNN / TensorRT line up). Do not reject an image for an
aN/bN/rcN PyTorch tag — NGC validates the full image; pick the newest
CUDA-aligned one and let compat-workarounds.md handle per-version issues. If the
matrix is unreachable, use the fallbacks in references/hardware-container.md;
default nvcr.io/nvidia/pytorch:24.09-py3 <!-- unpinned: documented fallback --> (driver ≥ 545; SDPA+GQA bug — if
num_key_value_heads < num_attention_heads, set attn_implementation: "eager").
Record ngc_image in config.yaml.
2c. Re-evaluate hardware-dependent compat rules: re-run the
compat-workarounds.md walk for entries whose detect needs hw; update
applicable_workarounds: in place.
2d. Model-fit check: estimate param_bytes ≈ 2×param_count (bf16); if
60% of
vram_gb_per_gpu × 1e9, recommend LoRA in the user-facing summary.
Gate: config.yaml has ngc_image, gpu_count, gpu_name, driver_major,
vram_gb_per_gpu; hardware-dependent compat fixes recorded.
Step 3 — Research the recipe
Goal: fetch the live recipe — training-data knowledge of
transformers/trl/peft is suspect, so Step 3 is non-negotiable. Walk
references/research-priorities.md in priority order (Priority 1 → 6); stop once
you have, for the detected task:
AutoModel/ processor classTrain + eval transforms
Collator
compute_metricsHyperparameter hints (LR, batch size, epochs, scheduler)
Record findings in meta/recipe.md, append source URLs to
config.yaml: research_sources:. A slot with no live finding falls back to the
matching scaffold (cv-scripts.md / vlm-scripts.md), logged as "fallback to
scaffold — no live source for <slot>" under notes:. Conflict-resolution rules
are in references/research-priorities.md.
Gate: every required slot filled, with a source URL or scaffold-fallback note.
Step 4 — Generate project & smoke-test
Goal: write all scripts, build the image, prepare data, run a 1-step smoke on
real data (one docker build, two docker runs).
4a. Generate project files in output_dir/: config.yaml, Dockerfile,
requirements.txt, prepare_data.py, train.py, run_eval.py, infer.py,
optional merge_lora.py, optional tests/, .gitignore. Live Step 3 research is
authority; cv-scripts.md / vlm-scripts.md give scaffold shape only. Apply every
applicable_workarounds entry as a Dockerfile block, requirement pin, config
override, or runtime env var. Hard rules: run_eval.py keeps that exact filename
(avoids colliding with the HF evaluate package); every generated .py starts
with the NVIDIA Apache-2.0 copyright header and any emitter fails when it is
missing; emit_unit_tests: true generates and runs tests per
references/testing.md. Script bodies, Dockerfile shape, and the emitter contract
are in references/workflow-generate-train.md.
4b. Build, prepare, smoke — docker build -t run-<short>:latest ., then
prepare_data and the --smoke --max_steps 1 run (references/docker-runs.md
§1-3). Smoke pass criteria (in logs/smoke.log):
No exception
Loss is finite (not
0.0, notNaN)grad_norm > 0at step 1
If emit_unit_tests: true, also run pytest tests/ in the container. Any failure → STOP.
4c. Preflight summary — before full training, print and verify: reference URL, dataset columns, Hub target, monitoring target, NGC image, hardware, smoke loss/grad norm.
Gate: project files written, image built, smoke PASSED, preflight has no blank fields.
Step 5 — Train, evaluate, infer
Goal: baseline eval, full training, post-train eval, optional LoRA merge, 5
inference samples (all commands: references/docker-runs.md §4-8).
| Sub-step | docker-runs.md | Skip if |
|---|---|---|
| 5a. Baseline eval (zero-shot) | §4 | skip_baseline: true |
| 5b. Full training (detached) | §5 | — |
| 5c. LoRA merge | §6 | not VLM+LoRA |
| 5d. Post-train eval | §7 | — |
| 5e. Inference (5 samples) | §8 | — |
Multi-GPU: prepend torchrun --nproc_per_node=$gpu_count to python train.py.
While training streams, watch docker logs -f hft_train: loss should drop within
10-20 steps; flat loss (collator/label-masking bug), NaN (LR too high), and OOM
all stop the run — recovery in references/core-rules.md. If emit_report: true,
run report.py after Step 5e per references/reporting.md.
Gate: all of:
checkpoints/final/(orcheckpoints/merged/for LoRA) existsreports/eval_results.jsonhas a numeric primary metricreports/baseline_results.jsonexists (unless skipped)reports/inference_samples/has 5 sampleswandb URL shows descending loss
Step 6 — Push & emit rerun skill
Goal: publish the run and make it reproducible without re-research.
Push per references/hub-push.md (weights, model card, eval/baseline JSONs,
config.yaml, Dockerfile, requirements.txt, inference samples, reports when
emitted) unless push_to_hub: false is explicit. Emit
<output_dir>/skills/run-<short>/SKILL.md from
references/pipeline-skill-template.md — substitute every placeholder, include
full YAML metadata + the NVIDIA copyright HTML comment, and make any emitter fail
if those are missing.
Gate (Done criteria): all of:
Step 5 gate met
HF Hub repo exists at the resolved URL with weights + card +
results/(unlesspush_to_hub: false)<output_dir>/skills/run-<short>/SKILL.mdexists, no<placeholder>left, with metadata + copyright HTML comment perpipeline-skill-template.md
Final message: wandb URL, HF Hub URL, baseline -> fine-tuned primary metric,
reports/inference_samples/, and the rerun skill path.
Error playbook
On a known runtime error, consult the symptom → minimal-fix table in
references/error-playbook.md (NGC entrypoint, PyTorch/Transformers regressions,
numpy ABI, Albumentations bbox, PEFT/checkpointing, LoRA target breadth, CV
augmentation gaps, OOM at step 0) before redesigning anything. When a row there
fires twice across runs, lift it into compat-workarounds.md with a detect rule
— auto-applied in Step 1 before the error can fire.
Communication style
Terse. No filler, no restating the request; one-word answers when appropriate.
Always include direct Hub and wandb URLs when referencing artifacts.
On error: state what went wrong, why, what you changed — no menus.
Never present "Option A/B/C" for a request with a clear answer. Act.
Files
- ATTRIBUTION.mdAdded by AutoGineer960 B
- BENCHMARK.md7.7 KiB
- LICENSELicence file added by AutoGineer11.2 KiB
- SKILL.md19.2 KiB
- config/skillspector-baseline.yaml4.4 KiB
- eval.config2.5 KiB
- evals/evals.json3.5 KiB
- references/compat-workarounds.md11.8 KiB
- references/core-rules.md4.7 KiB
- references/cv-scripts.md33.2 KiB
- references/dataset-patterns.md10.5 KiB
- references/dataset-recommendations.md11.0 KiB
- references/dataset-sources.md17.3 KiB
- references/deliverables.md20.1 KiB
- references/detailed-workflow.md715 B
- references/docker-runs.md8.9 KiB
- references/error-playbook.md3.8 KiB
- references/hardware-container.md8.7 KiB
- references/hub-push.md5.4 KiB
- references/model-discovery.md10.5 KiB
- references/pipeline-skill-template.md8.9 KiB
- references/progress-tracking.md9.7 KiB
- references/reporting.md18.5 KiB
- references/research-priorities.md6.5 KiB
- references/testing.md20.9 KiB
- references/vlm-scripts.md27.7 KiB
- references/workflow-generate-train.md7.6 KiB
- references/workflow-intake-preflight.md22.9 KiB
- references/workflow-push-rerun.md3.0 KiB
- skill-card.md4.9 KiB
- skill.oms.sig10.2 KiB
references/vlm-scripts.md
<!-- Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -->
VLM / LLM Pipeline Scripts Reference
Use this reference only when the parent SKILL.md points here for the current task. If this file conflicts with current SKILL.md, skill_info.yaml, schemas, or platform/model skills, the current authoritative source wins.
Contents
config.yaml — VLM Template
model.py
dataset.py
train.py
merge_lora.py
run_eval.py (NOT
evaluate.py— collides with HFevaluatelibrary)inference.py
VLM-Specific Gotchas
How to use this file
This file defines two things:
Structural scaffolding (marked
[SCAFFOLD]) — file names, entry point names, config schema, CLI boilerplate, LoRA target regex patterns, checkpoint saving. Copy these as-is.ML implementation stubs (marked
[FETCH LIVE]) — chat template formatting, processor call signatures, collator class, SFTTrainer/DPOTrainer kwargs, LoRA config. Do NOT copy. Fetch the live TRL/PEFT documentation and the specific model card instead.Why: VLM APIs change fast.
SFTTrainerkwargs, theprocessing_classparameter, chat template application, and LoRA target module names all vary by model family and TRL version. A stale template that worked for PaliGemma will silently break for Qwen2-VL or LLaVA-Next.Live doc URLs to fetch in Phase 4.2:
Training method Primary doc URL Secondary SFT (VLM/LLM) https://huggingface.co/docs/trl/sft_trainermodel card + model's own fine-tuning guide LoRA https://huggingface.co/docs/peft/quicktourhttps://huggingface.co/docs/peft/task_guides/image_classification_loraDPO https://huggingface.co/docs/trl/dpo_trainermodel card GRPO https://huggingface.co/docs/trl/grpo_trainermodel card Also fetch the model card for the specific
model_id: many VLMs (Qwen2-VL, LLaVA, PaliGemma) have their own fine-tuning guides linked from the card with exact processor usage, chat template format, and recommended LoRA targets.Search GitHub:
site:github.com {model_type} SFTTrainer fine-tunefor working examples.Rule: if the live doc's pattern contradicts anything in this file, the live doc wins. Log the discrepancy in PROGRESS.md with the doc URL.
config.yaml — VLM Template
# Model
model_id: google/paligemma-3b-pt-224
task: image-text-to-text
auto_model: AutoModelForImageTextToText
training_method: sft # sft | dpo | grpo
# LoRA
use_lora: true
lora_r: 16
lora_alpha: 32
lora_dropout: 0.05
lora_target_modules: ".*language_model.*\\.(q_proj|k_proj|v_proj|o_proj|gate_proj|up_proj|down_proj)"
# Dataset
dataset_id: lmms-lab/VQAv2
local_data_dir: ./data
n_train: 10000
n_eval: 1000
# Training
output_dir: ./checkpoints
num_train_epochs: 1
per_device_train_batch_size: 16
per_device_eval_batch_size: 8
learning_rate: 2.0e-4
warmup_ratio: 0.05
weight_decay: 0.01
lr_scheduler_type: cosine
bf16: true
gradient_checkpointing: false # disable with LoRA on A100 80GB; enable on smaller GPU
gradient_checkpointing_kwargs:
use_reentrant: false
max_grad_norm: 1.0
attn_implementation: eager # "sdpa" on NGC 25.01+, "eager" on 24.09
dataloader_num_workers: 4
dataloader_pin_memory: true
max_seq_length: 1024
image_max_soft_tokens: 140 # 70|140|280|560 — 140 is 2x faster than 280
# Evaluation
eval_strategy: epoch
save_strategy: epoch
load_best_model_at_end: false # not well-supported for generative models
metric_for_best_model: eval_loss
# Monitoring
report_to: wandb
logging_steps: 10
# Post-training
push_to_hub: false
model_short_name: paligemma-3b-vqamodel.py
import os
import yaml
import torch
from transformers import AutoProcessor, AutoModelForImageTextToText, AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, TaskType
def load_model_and_processor(cfg: dict):
model_id = cfg["model_id"]
task = cfg["task"]
token = os.environ.get("HF_TOKEN") or cfg.get("hf_token")
if task == "image-text-to-text":
ModelCls = AutoModelForImageTextToText
else:
ModelCls = AutoModelForCausalLM
# Dtype rule:
# - LoRA path: load base in bfloat16 (frozen base, trainable LoRA in fp32
# by default — saves ~2x VRAM, no underflow because gradients flow
# through fp32 LoRA weights, not the frozen bf16 base).
# - Full fine-tune: load in float32. `TrainingArguments(bf16=True)` does
# mixed-precision casting via autocast; loading the base in bfloat16
# AND enabling bf16 training causes optimizer-state underflow and the
# "loss stays near random" symptom documented in the master gotcha
# index.
use_lora = cfg.get("use_lora", True)
if use_lora and cfg.get("bf16", True):
load_dtype = torch.bfloat16
else:
load_dtype = torch.float32
model = ModelCls.from_pretrained(
model_id,
torch_dtype=load_dtype,
device_map="auto",
attn_implementation=cfg.get("attn_implementation", "eager"),
token=token,
)
if task == "image-text-to-text":
processor = AutoProcessor.from_pretrained(model_id, token=token)
else:
from transformers import AutoTokenizer
processor = AutoTokenizer.from_pretrained(model_id, token=token)
if processor.pad_token is None:
processor.pad_token = processor.eos_token
if cfg.get("use_lora", True):
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=cfg.get("lora_r", 16),
lora_alpha=cfg.get("lora_alpha", 32),
lora_dropout=cfg.get("lora_dropout", 0.05),
target_modules=cfg.get("lora_target_modules",
r".*language_model.*\.(q_proj|k_proj|v_proj|o_proj|gate_proj|up_proj|down_proj)"),
bias="none",
)
model = get_peft_model(model, lora_config)
trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
pct = 100 * trainable / total
print(f"Trainable: {trainable / 1e6:.1f}M / {total / 1e6:.1f}M params ({pct:.2f}%)")
if pct > 5 and cfg.get("use_lora"):
print("WARNING: >5% trainable with LoRA — check lora_target_modules regex")
return model, processordataset.py
import os
import yaml
import torch
from datasets import load_from_disk, Image as HFImage
from torch.utils.data import Dataset
class VLMDataset(Dataset):
"""Supports VQA-style datasets with image + question + answer columns."""
def __init__(self, arrow_path: str, processor, cfg: dict):
self.ds = load_from_disk(arrow_path)
if "image" in self.ds.column_names:
self.ds = self.ds.cast_column("image", HFImage())
self.processor = processor
self.cfg = cfg
self.max_length = cfg.get("max_seq_length", 1024)
self._verify_collator()
def _verify_collator(self):
if len(self.ds) < 2:
return
samples = [self.__getitem__(i) for i in range(2)]
non_masked = (samples[0]["labels"] != -100).sum().item()
total = samples[0]["labels"].numel()
print(f"Collator check — non-masked labels: {non_masked}/{total} ({100*non_masked/total:.1f}%)")
if non_masked == 0:
raise ValueError("COLLATOR ERROR: all labels are masked (-100). Check prompt/answer boundary logic.")
def _build_messages(self, item: dict) -> list:
if "messages" in item:
return item["messages"]
# VQA-style: image + question + answer
question = item.get("question", "")
answer = item.get("answers", [""])[0] if isinstance(item.get("answers"), list) else item.get("answer", "")
return [
{"role": "user", "content": [
{"type": "image"},
{"type": "text", "text": question},
]},
{"role": "assistant", "content": answer},
]
def __len__(self):
return len(self.ds)
def __getitem__(self, idx):
"""Return a DICT of tensors — DO NOT pad or truncate here. Let `collate_vlm`
handle batching (images in VLMs like Idefics3 expand to hundreds of image
tokens; mid-image truncation breaks the processor with:
ValueError: Mismatch in `image` token count between text and `input_ids`
)."""
item = self.ds[idx]
image = item["image"].convert("RGB")
messages = self._build_messages(item)
prompt_msgs = messages[:-1]
prompt = self.processor.apply_chat_template(prompt_msgs, add_generation_prompt=True, tokenize=False)
full = self.processor.apply_chat_template(messages, tokenize=False)
# No padding/truncation at sample level — variable length is fine
inputs = self.processor(text=full, images=image, return_tensors="pt")
inputs = {k: v.squeeze(0) for k, v in inputs.items()}
# Mask prompt tokens
prompt_enc = self.processor(text=prompt, images=image, return_tensors="pt")
prompt_len = prompt_enc["input_ids"].shape[1]
labels = inputs["input_ids"].clone()
labels[:prompt_len] = -100
labels[labels == self.processor.tokenizer.pad_token_id] = -100
inputs["labels"] = labels
return inputs
def collate_vlm(batch, pad_token_id: int = 0):
"""Batch VLM samples with heterogeneous shapes.
Text tensors (input_ids, attention_mask, labels) are padded to batch-max length.
Image tensors (pixel_values, pixel_attention_mask) are padded along both
`num_images` (variable per sample for models that tile high-res inputs like
Idefics3) AND spatial dims if they differ.
"""
import torch
# --- Text side: pad to longest in batch ---
max_seq = max(b["input_ids"].shape[0] for b in batch)
def _pad_1d(t, length, value):
if t.shape[0] >= length:
return t[:length]
return torch.cat([t, torch.full((length - t.shape[0],), value, dtype=t.dtype)])
out = {
"input_ids": torch.stack([_pad_1d(b["input_ids"], max_seq, pad_token_id) for b in batch]),
"attention_mask": torch.stack([_pad_1d(b["attention_mask"], max_seq, 0) for b in batch]),
"labels": torch.stack([_pad_1d(b["labels"], max_seq, -100) for b in batch]),
}
# --- Image side: pad num_images, then spatial dims if they differ ---
if "pixel_values" in batch[0]:
pvs = [b["pixel_values"] for b in batch] # each: (n_img, C, H, W) for Idefics3
# Ensure 4D (n_img, C, H, W) — if 3D (C, H, W), add n_img=1 dim
pvs = [pv.unsqueeze(0) if pv.ndim == 3 else pv for pv in pvs]
max_n = max(pv.shape[0] for pv in pvs)
max_h = max(pv.shape[-2] for pv in pvs)
max_w = max(pv.shape[-1] for pv in pvs)
def _pad_img(pv):
n, c, h, w = pv.shape
if (h, w) != (max_h, max_w):
pv = torch.nn.functional.pad(pv, (0, max_w - w, 0, max_h - h), value=0.0)
if n < max_n:
pv = torch.cat([pv, torch.zeros(max_n - n, c, max_h, max_w, dtype=pv.dtype)], dim=0)
return pv
out["pixel_values"] = torch.stack([_pad_img(pv) for pv in pvs])
# pixel_attention_mask if processor produced one
if "pixel_attention_mask" in batch[0]:
pams = [b["pixel_attention_mask"] for b in batch]
pams = [p.unsqueeze(0) if p.ndim == 2 else p for p in pams]
max_n = max(p.shape[0] for p in pams)
max_h = max(p.shape[-2] for p in pams)
max_w = max(p.shape[-1] for p in pams)
def _pad_mask(p):
n, h, w = p.shape
if (h, w) != (max_h, max_w):
p = torch.nn.functional.pad(p, (0, max_w - w, 0, max_h - h), value=0)
if n < max_n:
p = torch.cat([p, torch.zeros(max_n - n, max_h, max_w, dtype=p.dtype)], dim=0)
return p
out["pixel_attention_mask"] = torch.stack([_pad_mask(p) for p in pams])
return out
class LLMDataset(Dataset):
"""Text-only SFT dataset for LLM training."""
def __init__(self, arrow_path: str, tokenizer, max_length: int = 1024):
self.ds = load_from_disk(arrow_path)
self.tokenizer = tokenizer
self.max_length = max_length
def __len__(self):
return len(self.ds)
def __getitem__(self, idx):
item = self.ds[idx]
if "messages" in item:
text = self.tokenizer.apply_chat_template(item["messages"], tokenize=False)
else:
text = item.get("text") or item.get("prompt", "") + item.get("completion", "")
enc = self.tokenizer(text, max_length=self.max_length, truncation=True,
padding="max_length", return_tensors="pt")
enc = {k: v.squeeze(0) for k, v in enc.items()}
enc["labels"] = enc["input_ids"].clone()
return enctrain.py
import argparse
import os
import yaml
import torch
from transformers import TrainingArguments
from trl import SFTTrainer, SFTConfig, DPOTrainer, DPOConfig
from model import load_model_and_processor
from dataset import VLMDataset, LLMDataset
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--config", default="config.yaml")
return p.parse_args()
def train_sft(cfg, model, processor):
task = cfg["task"]
if task == "image-text-to-text":
DatasetCls = lambda path: VLMDataset(path, processor, cfg)
else:
DatasetCls = lambda path: LLMDataset(path, processor, cfg.get("max_seq_length", 1024))
train_ds = DatasetCls(f"{cfg['local_data_dir']}/train")
eval_ds = DatasetCls(f"{cfg['local_data_dir']}/eval")
smoke = bool(cfg.get("smoke_test", False))
if smoke:
os.environ["WANDB_MODE"] = "disabled"
sft_args = SFTConfig(
output_dir=cfg["output_dir"],
num_train_epochs=cfg["num_train_epochs"],
per_device_train_batch_size=cfg["per_device_train_batch_size"],
per_device_eval_batch_size=cfg.get("per_device_eval_batch_size", 8),
learning_rate=cfg["learning_rate"],
warmup_ratio=cfg.get("warmup_ratio", 0.05),
weight_decay=cfg.get("weight_decay", 0.01),
lr_scheduler_type=cfg.get("lr_scheduler_type", "cosine"),
bf16=cfg.get("bf16", True),
gradient_checkpointing=cfg.get("gradient_checkpointing", False),
gradient_checkpointing_kwargs=cfg.get("gradient_checkpointing_kwargs", {"use_reentrant": False}),
max_grad_norm=cfg.get("max_grad_norm", 1.0),
dataloader_num_workers=cfg.get("dataloader_num_workers", 4),
dataloader_pin_memory=cfg.get("dataloader_pin_memory", True),
max_length=cfg.get("max_seq_length", 1024), # SFTConfig uses max_length
max_steps=1 if smoke else -1,
eval_strategy="no" if smoke else cfg.get("eval_strategy", "epoch"),
save_strategy="no" if smoke else cfg.get("save_strategy", "epoch"),
report_to="none" if smoke else cfg.get("report_to", "wandb"),
logging_steps=1 if smoke else cfg.get("logging_steps", 10),
run_name=os.environ.get("WANDB_RUN_NAME"),
dataset_kwargs={"skip_prepare_dataset": True}, # use pre-tokenized dataset
)
trainer = SFTTrainer(
model=model,
args=sft_args,
train_dataset=train_ds,
eval_dataset=eval_ds,
)
trainer.train()
if smoke:
# Find the step-level log entry. The final entry is the training summary
# which carries `train_loss` (not `loss`); the step entries have `loss`
# and `grad_norm`. Searching by key avoids the off-by-one.
step_log = next(
(l for l in reversed(trainer.state.log_history) if "loss" in l), None
)
if step_log is None:
raise RuntimeError("smoke test produced no step-level log entry")
loss = step_log["loss"]
grad_norm = step_log.get("grad_norm", 0.0)
print(f"SMOKE: step={step_log.get('step')} loss={loss:.4f} grad_norm={grad_norm:.4f}")
if not (loss == loss) or loss == 0.0 or grad_norm == 0.0: # NaN-safe
raise RuntimeError(
f"smoke test failed: loss={loss}, grad_norm={grad_norm} — "
"labels/masking bug; do not proceed to full training"
)
return
trainer.save_model(f"{cfg['output_dir']}/final")
processor.save_pretrained(f"{cfg['output_dir']}/final")
def train_dpo(cfg, model, processor):
from datasets import load_from_disk
train_ds = load_from_disk(f"{cfg['local_data_dir']}/train")
eval_ds = load_from_disk(f"{cfg['local_data_dir']}/eval")
# DPO requires prompt, chosen, rejected columns
for col in ["prompt", "chosen", "rejected"]:
assert col in train_ds.column_names, f"DPO requires '{col}' column"
dpo_args = DPOConfig(
output_dir=cfg["output_dir"],
num_train_epochs=cfg["num_train_epochs"],
per_device_train_batch_size=cfg["per_device_train_batch_size"],
learning_rate=cfg.get("learning_rate", 5e-7),
bf16=cfg.get("bf16", True),
report_to=cfg.get("report_to", "wandb"),
logging_steps=cfg.get("logging_steps", 10),
run_name=os.environ.get("WANDB_RUN_NAME"),
)
trainer = DPOTrainer(
model=model,
ref_model=None, # None → uses implicit ref from PEFT frozen params
args=dpo_args,
train_dataset=train_ds,
eval_dataset=eval_ds,
processing_class=processor,
)
trainer.train()
trainer.save_model(f"{cfg['output_dir']}/final")
def main():
args = parse_args()
with open(args.config) as f:
cfg = yaml.safe_load(f)
model, processor = load_model_and_processor(cfg)
method = cfg.get("training_method", "sft")
if method == "sft":
train_sft(cfg, model, processor)
elif method == "dpo":
train_dpo(cfg, model, processor)
else:
raise ValueError(f"Unknown training_method: {method}. Use sft | dpo | grpo")
print(f"Training complete ({method}). Model saved to {cfg['output_dir']}/final")
if __name__ == "__main__":
main()merge_lora.py
import argparse
import os
import torch
from peft import PeftModel
from transformers import AutoModelForImageTextToText, AutoModelForCausalLM, AutoProcessor, AutoTokenizer
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--base_model", required=True)
p.add_argument("--adapter_path", required=True)
p.add_argument("--output_path", required=True)
p.add_argument("--task", default="image-text-to-text")
return p.parse_args()
def main():
args = parse_args()
token = os.environ.get("HF_TOKEN")
print(f"Loading base model: {args.base_model}")
if args.task == "image-text-to-text":
base = AutoModelForImageTextToText.from_pretrained(
args.base_model, torch_dtype=torch.bfloat16, device_map="auto", token=token)
proc = AutoProcessor.from_pretrained(args.base_model, token=token)
else:
base = AutoModelForCausalLM.from_pretrained(
args.base_model, torch_dtype=torch.bfloat16, device_map="auto", token=token)
proc = AutoTokenizer.from_pretrained(args.base_model, token=token)
print(f"Loading LoRA adapter: {args.adapter_path}")
model = PeftModel.from_pretrained(base, args.adapter_path)
print("Merging LoRA weights into base model...")
merged = model.merge_and_unload()
print(f"Saving merged model to: {args.output_path}")
merged.save_pretrained(args.output_path, safe_serialization=True)
proc.save_pretrained(args.output_path)
print("Merge complete.")
if __name__ == "__main__":
main()run_eval.py (NOT evaluate.py — collides with HF evaluate library)
import argparse
import json
import os
import re
import yaml
import torch
from datasets import load_from_disk, Image as HFImage
from transformers import AutoProcessor, AutoModelForImageTextToText, AutoModelForCausalLM
from tqdm import tqdm
def normalize_answer(s: str) -> str:
s = s.lower().strip()
s = re.sub(r"[^\w\s]", "", s)
s = re.sub(r"\b(a|an|the)\b", " ", s)
return " ".join(s.split())
def vqa_accuracy(predicted: str, human_answers: list) -> float:
pred_norm = normalize_answer(predicted)
count = sum(1 for a in human_answers if normalize_answer(a) == pred_norm)
return min(1.0, count / 3.0)
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--config", default="config.yaml")
p.add_argument("--checkpoint", required=True)
p.add_argument("--output", default="reports/eval_results.json")
return p.parse_args()
def main():
args = parse_args()
with open(args.config) as f:
cfg = yaml.safe_load(f)
token = os.environ.get("HF_TOKEN")
task = cfg["task"]
if task == "image-text-to-text":
model = AutoModelForImageTextToText.from_pretrained(
args.checkpoint, torch_dtype=torch.bfloat16, device_map="auto", token=token)
processor = AutoProcessor.from_pretrained(args.checkpoint, token=token)
else:
from transformers import AutoTokenizer
model = AutoModelForCausalLM.from_pretrained(
args.checkpoint, torch_dtype=torch.bfloat16, device_map="auto", token=token)
processor = AutoTokenizer.from_pretrained(args.checkpoint, token=token)
model.eval()
eval_ds = load_from_disk(f"{cfg['local_data_dir']}/eval")
if "image" in eval_ds.column_names:
eval_ds = eval_ds.cast_column("image", HFImage())
scores = []
for item in tqdm(eval_ds, desc="Evaluating"):
image = item["image"].convert("RGB") if task == "image-text-to-text" else None
question = item.get("question", "")
ground_truth = item.get("answers", [item.get("answer", "")])
if isinstance(ground_truth, str):
ground_truth = [ground_truth]
if task == "image-text-to-text":
messages = [{"role": "user", "content": [
{"type": "image"}, {"type": "text", "text": question}]}]
prompt = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
inputs = processor(text=prompt, images=image, return_tensors="pt").to(model.device)
else:
inputs = processor(question, return_tensors="pt").to(model.device)
with torch.no_grad():
out_ids = model.generate(
**inputs,
max_new_tokens=32,
do_sample=False, # greedy — deterministic for eval
pad_token_id=processor.tokenizer.pad_token_id if hasattr(processor, "tokenizer") else processor.pad_token_id,
)
prompt_len = inputs["input_ids"].shape[1]
answer = processor.decode(out_ids[0][prompt_len:], skip_special_tokens=True).strip()
score = vqa_accuracy(answer, ground_truth)
scores.append(score)
results = {
"vqa_accuracy": sum(scores) / len(scores),
"n_eval": len(scores),
"method": cfg.get("training_method", "sft"),
"model_id": cfg["model_id"],
"checkpoint": args.checkpoint,
}
os.makedirs(os.path.dirname(args.output), exist_ok=True)
with open(args.output, "w") as f:
json.dump(results, f, indent=2)
print("Eval results:", json.dumps(results, indent=2))
print(f"\nVQA Accuracy: {results['vqa_accuracy']:.4f} ({results['vqa_accuracy']*100:.2f}%)")
if __name__ == "__main__":
main()inference.py
import argparse
import json
import os
import yaml
import torch
from datasets import load_from_disk, Image as HFImage
from pathlib import Path
from transformers import AutoProcessor, AutoModelForImageTextToText
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--config", default="config.yaml")
p.add_argument("--checkpoint", required=True)
p.add_argument("--n_samples", type=int, default=5)
p.add_argument("--output", default="reports/inference_samples")
return p.parse_args()
def main():
args = parse_args()
with open(args.config) as f:
cfg = yaml.safe_load(f)
token = os.environ.get("HF_TOKEN")
model = AutoModelForImageTextToText.from_pretrained(
args.checkpoint, torch_dtype=torch.bfloat16, device_map="auto", token=token)
processor = AutoProcessor.from_pretrained(args.checkpoint, token=token)
model.eval()
eval_ds = load_from_disk(f"{cfg['local_data_dir']}/eval")
if "image" in eval_ds.column_names:
eval_ds = eval_ds.cast_column("image", HFImage())
out_dir = Path(args.output)
out_dir.mkdir(parents=True, exist_ok=True)
for i in range(min(args.n_samples, len(eval_ds))):
item = eval_ds[i]
image = item["image"].convert("RGB")
question = item.get("question", "Describe this image.")
ground_truth = item.get("answers", [item.get("answer", "")])
messages = [{"role": "user", "content": [
{"type": "image"}, {"type": "text", "text": question}]}]
prompt = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
inputs = processor(text=prompt, images=image, return_tensors="pt").to(model.device)
with torch.no_grad():
out_ids = model.generate(**inputs, max_new_tokens=64, do_sample=False)
prompt_len = inputs["input_ids"].shape[1]
answer = processor.decode(out_ids[0][prompt_len:], skip_special_tokens=True).strip()
image.save(out_dir / f"sample_{i}_input.jpg")
meta = {
"question": question,
"ground_truth": ground_truth,
"predicted": answer,
}
with open(out_dir / f"sample_{i}_meta.json", "w") as f:
json.dump(meta, f, indent=2)
print(f"Sample {i}: Q={question!r} | GT={ground_truth} | Pred={answer!r}")
if __name__ == "__main__":
main()VLM-Specific Gotchas
GOTCHA: dtype= vs torch_dtype=
Use torch_dtype=torch.bfloat16, NOT dtype=. Wrong key silently loads in float32.
HANDLED: dtype rule for full fine-tune vs LoRA
The template loads the base in bfloat16 only when use_lora=True and
bf16=True. For full fine-tune (use_lora=False), the base loads in
float32 so TrainingArguments(bf16=True) autocast works correctly.
Loading the base in bfloat16 AND enabling bf16 training causes
optimizer-state underflow ("loss stays near random") for full fine-tunes.
GOTCHA: LoRA on VLMs — exclude vision encoder
Many VLMs (Gemma4, LLaVA, PaliGemma) use custom linear types in the vision encoder that PEFT cannot wrap.
Always use regex: ".*language_model.*\\.(q_proj|k_proj|v_proj|o_proj|gate_proj|up_proj|down_proj)"
GOTCHA: transformers>=5.0 for 2024+ VLMs
PaliGemma 2, Gemma 3/4, LLaVA-Next, Qwen2-VL require transformers>=5.0.0.
GOTCHA: SFTConfig uses max_length, not max_seq_length
TRL SFTConfig parameter is max_length. Using max_seq_length is silently ignored.
GOTCHA: trl >= 1.0 breaking API
Pin trl>=0.18.0,<1.0.0 for stability. TRL 1.0+ has breaking changes to SFTTrainer/DPOTrainer.
GOTCHA: dataset_kwargs={"skip_prepare_dataset": True}
When using pre-tokenized datasets (VLMDataset returns tensors), pass this to SFTTrainer to prevent
it from trying to tokenize again (it doesn't know about vision inputs).
Expected baselines (VQA v2, 10K train samples, 1 epoch):
Zero-shot (no finetuning): 55-65% accuracy
After LoRA SFT: 58-73% accuracy (+3-8%)
Full finetune on 443K samples: 75-80% accuracy
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Needs an API key, Needs Python
The author says: Requires docker + nvidia-container-toolkit, NVIDIA GPU (driver ≥ 545, ≥ 24 GB VRAM for ≤3B models), ~40 GB free disk. Optional credentials (read from the session environment) — HF_TOKEN is read only when the model/dataset is gated or `push_to_hub` is on; WANDB_API_KEY and WANDB_PROJECT only when WandB logging is enabled.
Security
What our scanner noticed· Medium risk
- Large base64-encoded blobs
- Instructions that use the network
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- NVIDIA Corporation
- Publisher
- NVIDIA (Official)
- Source
- NVIDIA/skills · skills/tao-finetune-huggingface-model · commit fce3cca
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE-APACHE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by NVIDIA Corporation or NVIDIA.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download tao-finetune-huggingface-model.zip