Generated banner for Tao Finetune Huggingface Model
Generated logo for Tao Finetune Huggingface Model

Tao Finetune Huggingface Model

NVIDIAOfficialCreated by NVIDIA Corporation

  • Skill
  • Workflow
  • Software development
  • Apache-2.0
  • Medium risk

From the author

Fine-tune any HuggingFace CV / VLM / LLM model on local NVIDIA GPUs inside an NGC PyTorch container when no dedicated TAO model skill matches. Use when the user wants to fine-tune a HuggingFace model (full or LoRA), train a vision / VLM / LLM model end-to-end, generate a reproducible HF training pipeline, smoke-test a HuggingFace model locally before scale-up, push a fine-tuned model to the HF Hub with a model card, or emit a self-contained rerun skill for an existing HuggingFace finetune. Supports image classification, object detection, semantic / instance / panoptic segmentation, depth estimation, image-text-to-text VLM (SFT / LoRA), and LLM SFT / DPO / GRPO. Six-step workflow: inspect and qualify, hardware and NGC image, research, generate and smoke, train + eval + infer, push and emit rerun skill. Do not use for any Hugging Face model ID claimed by a dedicated `skills/models/*` skill; the model skill and its declared execution environment take precedence.
— the description by NVIDIA Corporation

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

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/nvidia/tao-finetune-huggingface-model/download
  2. Unzip 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-setup skill 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 json

The 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):

  1. User input — explicit model_id, dataset_id, training_method, config.yaml overrides.

  2. Live research — model card, HF repo example, author finetune script, HF task docs, paper; always fetched (Step 3 + references/research-priorities.md).

  3. Curated references (references/*.md) — fallback when live research is silent/ambiguous.

  4. 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) or push_to_hub is on (write); public + public + push_to_hub: false needs none. Value never read — presence-only via [ -n "$HF_TOKEN" ].

  • WANDB_API_KEY, WANDB_PROJECT — only when WandB is enabled; WANDB_MODE=disabled opts out.

Dataset — exactly one:

  • dataset_id — HuggingFace dataset ID (source: hf)

  • local_dataset_path — local folder or file (source: local); optional local_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 card

  • n_train=10000, n_eval=1000, n_epochs=3, lora_r=16

  • output_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 to False to skip

  • skip_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 tests

All 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.

ConcernAuthoritative 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 1 before 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 from architectures + tags + card examples (fallback logging in model-discovery.md).

  • Dataset: for recommended datasets, first present 3-5 choices from dataset-recommendations.md; for local data, bind-mount read-only and use dataset-sources.md format 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.md against 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):

  1. GPU host runtime — tao-setup-nvidia-gpu-host's setup-nvidia-gpu-host.sh --backend docker --check-only; on fail, ask approval then re-run with --install --yes.

  2. Free-disk soft-warn — override via MIN_DISK_GB (default 100 GB); recommend ≥ 100 GB for NGC base (~20 GB) + HF cache + checkpoints + data.

  3. Conditional credential presence (values never read) — HF_TOKEN only when gated or push_to_hub is 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 class

  • Train + eval transforms

  • Collator

  • compute_metrics

  • Hyperparameter 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, not NaN)

  • grad_norm > 0 at 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-stepdocker-runs.mdSkip if
5a. Baseline eval (zero-shot)§4skip_baseline: true
5b. Full training (detached)§5—
5c. LoRA merge§6not 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/ (or checkpoints/merged/ for LoRA) exists

  • reports/eval_results.json has a numeric primary metric

  • reports/baseline_results.json exists (unless skipped)

  • reports/inference_samples/ has 5 samples

  • wandb 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/ (unless push_to_hub: false)

  • <output_dir>/skills/run-<short>/SKILL.md exists, no <placeholder> left, with metadata + copyright HTML comment per pipeline-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

references/cv-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. -->

CV 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

  • requirements.txt — CV Template

  • config.yaml — CV Template

  • model.py

  • dataset.py

  • train.py

  • run_eval.py (NOT evaluate.py — collides with HF evaluate library)

  • inference.py

  • Detection-Specific Gotchas

  • Per-Task Metrics Summary

How to use this file

This file defines two things:

  1. Structural scaffolding (marked [SCAFFOLD]) — file names, entry point names, config schema, CLI boilerplate, logging setup, checkpoint saving. Copy these as-is.

  2. ML implementation stubs (marked [FETCH LIVE]) — preprocessing transforms, model loading kwargs, collator choice, training loop, metrics. Do NOT copy these. Instead, fetch the canonical live HuggingFace documentation for the task and use that.

Why: templates go stale. The HF docs are maintained by the model authors. The augmentation pattern, collator class, and compute_metrics signature change across model families and transformers versions. Empirical test: using a static template without augmentation gave 57% accuracy; using the live HF image_classification tutorial pattern gave 94% on the same data.

Live doc URLs to fetch in Phase 4.2:

TaskPrimary doc URLSecondary
image-classificationhttps://huggingface.co/docs/transformers/tasks/image_classificationmodel card on HF Hub
object-detectionhttps://huggingface.co/docs/transformers/tasks/object_detectionmodel card
semantic-segmentationhttps://huggingface.co/docs/transformers/tasks/semantic_segmentationmodel card
instance-segmentationhttps://huggingface.co/docs/transformers/tasks/instance_segmentationmodel card
depth-estimationhttps://huggingface.co/docs/transformers/tasks/monocular_depth_estimationmodel card

Also search GitHub: site:github.com transformers {model_type} fine-tune train.py and inspect the top result's preprocessing section before writing any transforms.

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.


requirements.txt — CV Template

# Core HF stack (unpinned — let the NGC base image's transformers win unless a
# compat-workaround forces a pin in the Dockerfile post-install).
transformers
accelerate
datasets
evaluate

# Vision backbones. `timm` is required by several transformers vision models
# whose default ResNet/ConvNeXt backbones go through timm (DETR family,
# Conditional/Deformable DETR, BEiT, ViTMatte, OneFormer, ...). Cheap to
# include and avoids "ImportError: requires the timm library" on first load.
timm
torchvision

# Detection / segmentation metrics.
torchmetrics
pycocotools

# Reporting.
matplotlib
Pillow
pyyaml
tqdm

# Tests (Phase 4.5).
pytest>=7.0

Why unpinned core? The NGC base image ships pinned torch/transformers/ accelerate for a known-good driver/CUDA combo. Pinning here often forces pip to downgrade the NGC versions and break the build. The compat-workarounds.md registry adds version pins via dockerfile_block only when a known incompatibility is detected.


config.yaml — CV Template

# Model
model_id: google/vit-base-patch16-224
task: image-classification          # image-classification | object-detection | semantic-segmentation
auto_model: AutoModelForImageClassification

# Dataset
dataset_id: imagenet-1k
local_data_dir: ./data
n_train: 10000
n_eval: 1000

# Training
output_dir: ./checkpoints
num_train_epochs: 3
per_device_train_batch_size: 32
per_device_eval_batch_size: 64
learning_rate: 5.0e-5
head_learning_rate: 3.0e-3   # image-classification: faster newly initialized head
warmup_ratio: 0.1
weight_decay: 0.01
lr_scheduler_type: cosine
bf16: true
gradient_checkpointing: false
dataloader_num_workers: 4
dataloader_pin_memory: true
remove_unused_columns: false

# Evaluation
eval_strategy: epoch
save_strategy: epoch
load_best_model_at_end: true
metric_for_best_model: accuracy
greater_is_better: true

# Monitoring
report_to: wandb
logging_steps: 10

# Packaging
model_short_name: vit-base-imagenet

model.py

import yaml
import torch
from transformers import (
    AutoConfig,
    AutoImageProcessor,
    AutoModelForImageClassification,
    AutoModelForObjectDetection,
    AutoModelForSemanticSegmentation,
    AutoModelForDepthEstimation,
)

_AUTO_MODEL_MAP = {
    "image-classification": AutoModelForImageClassification,
    "object-detection": AutoModelForObjectDetection,
    "semantic-segmentation": AutoModelForSemanticSegmentation,
    "depth-estimation": AutoModelForDepthEstimation,
}


def load_model_and_processor(cfg: dict):
    model_id = cfg["model_id"]
    task = cfg["task"]
    token = cfg.get("hf_token") or None

    processor = AutoImageProcessor.from_pretrained(model_id, token=token)

    # Build id2label / label2id from dataset feature metadata
    id2label = cfg.get("id2label", {})
    label2id = {v: k for k, v in id2label.items()} if id2label else {}

    AutoModelCls = _AUTO_MODEL_MAP[task]
    # Load in float32 — let TrainingArguments(bf16=True) handle mixed precision.
    # Loading in bfloat16 AND enabling bf16 training causes optimizer underflow.
    model = AutoModelCls.from_pretrained(
        model_id,
        token=token,
        ignore_mismatched_sizes=True,   # safe for label count changes
        **({"id2label": id2label, "label2id": label2id} if id2label else {}),
    )

    trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
    total = sum(p.numel() for p in model.parameters())
    print(f"Trainable: {trainable / 1e6:.1f}M / {total / 1e6:.1f}M params ({100 * trainable / total:.1f}%)")

    return model, processor

dataset.py

import yaml
import torch
from datasets import load_from_disk, Image as HFImage
from torch.utils.data import Dataset
from torchvision.transforms import (
    CenterCrop, Compose, Normalize,
    RandomHorizontalFlip, RandomResizedCrop, RandAugment, Resize, ToTensor,
)


def make_classification_transforms(processor, is_train: bool):
    """Build augmentation pipeline from processor's normalization stats.

    Training uses RandomResizedCrop + RandomHorizontalFlip + RandAugment — critical for
    small datasets and ConvNeXt/ViT fine-tunes. Keep eval deterministic; do not rely on
    AutoImageProcessor resize+normalize alone for paper-level classification accuracy.
    Eval uses deterministic Resize + CenterCrop.
    """
    if "shortest_edge" in processor.size:
        size = processor.size["shortest_edge"]
    else:
        size = processor.size["height"]
    normalize = Normalize(mean=processor.image_mean, std=processor.image_std)

    if is_train:
        return Compose([
            RandomResizedCrop(size),
            RandomHorizontalFlip(),
            RandAugment(num_ops=2, magnitude=9),
            ToTensor(),
            normalize,
        ])
    else:
        return Compose([
            Resize(size),
            CenterCrop(size),
            ToTensor(),
            normalize,
        ])


class CVDataset(Dataset):
    def __init__(self, arrow_path: str, processor, task: str,
                 is_train: bool = False, id2label: dict = None):
        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.task = task
        self.id2label = id2label or {}
        self.label_col = "labels" if "labels" in self.ds.column_names else "label"

        if task == "image-classification":
            self.transform = make_classification_transforms(processor, is_train)
        else:
            self.transform = None

    def __len__(self):
        return len(self.ds)

    def __getitem__(self, idx):
        item = self.ds[idx]
        image = item["image"].convert("RGB")

        if self.task == "image-classification":
            pixel_values = self.transform(image)
            return {
                "pixel_values": pixel_values,
                "labels": torch.tensor(item[self.label_col], dtype=torch.long),
            }

        elif self.task == "object-detection":
            objects = item["objects"]
            annotations = {
                "image_id": idx,
                "annotations": [
                    {
                        "bbox": bbox,
                        "category_id": cat_id,
                        "iscrowd": 0,
                        "area": bbox[2] * bbox[3],  # w * h
                    }
                    for bbox, cat_id in zip(objects["bbox"], objects["category_id"])
                ],
            }
            inputs = self.processor(
                images=image,
                annotations=annotations,
                return_tensors="pt",
            )
            # Detection processors return `pixel_values` with a leading batch dim
            # of 1, but `labels` is already a list-of-1 dict whose tensors have
            # no batch dim. Squeezing the dict uniformly breaks scalar label
            # tensors (shape (1,) → 0-dim scalar → "zero-dimensional tensor
            # cannot be concatenated" in loss). Handle each key explicitly.
            inputs = {
                "pixel_values": inputs["pixel_values"].squeeze(0),
                "labels": inputs["labels"][0],
            }

        elif self.task == "semantic-segmentation":
            mask = item.get("annotation") or item.get("mask")
            inputs = self.processor(images=image, segmentation_maps=mask, return_tensors="pt")
            inputs = {k: v.squeeze(0) for k, v in inputs.items()}

        elif self.task == "depth-estimation":
            depth = item.get("depth") or item.get("depth_map")
            inputs = self.processor(images=image, return_tensors="pt")
            inputs = {k: v.squeeze(0) for k, v in inputs.items()}
            inputs["labels"] = torch.tensor(depth, dtype=torch.float32)

        return inputs


def make_collate_fn_detection(processor):
    """Detection collator factory — version-agnostic manual pad.

    Most object-detection processors (DETR, Conditional/Deformable DETR, RT-DETR,
    Mask2Former, etc.) resize images independently per sample, so naive
    `torch.stack(pixel_values)` fails with "stack expects equal size".

    transformers 4.x exposed `processor.pad(images, return_tensors="pt")` as a
    batch-pad helper; transformers 5.x removed that overload (`pad` is now a
    per-image API: `pad(image, padded_size, ...)`). Doing the pad manually with
    `torch.nn.functional.pad` works on both 4.x and 5.x and produces the same
    output the 4.x batch-pad produced internally. Labels stay as a list-of-dicts
    (variable bbox count per image).
    """
    del processor  # unused — kept for signature stability so callers don't change
    def collate_fn(batch):
        pixel_values = [b["pixel_values"] for b in batch]
        labels = [b["labels"] for b in batch]
        max_h = max(pv.shape[-2] for pv in pixel_values)
        max_w = max(pv.shape[-1] for pv in pixel_values)
        padded, masks = [], []
        for pv in pixel_values:
            c, h, w = pv.shape
            padded.append(torch.nn.functional.pad(pv, (0, max_w - w, 0, max_h - h), value=0.0))
            mask = torch.zeros(max_h, max_w, dtype=torch.long)
            mask[:h, :w] = 1
            masks.append(mask)
        return {
            "pixel_values": torch.stack(padded),
            "pixel_mask": torch.stack(masks),
            "labels": labels,
        }
    return collate_fn

train.py

import argparse
import os
import yaml
import evaluate
import numpy as np
import torch
from transformers import TrainingArguments, Trainer
from model import load_model_and_processor
from dataset import CVDataset, make_collate_fn_detection


def parse_args():
    p = argparse.ArgumentParser()
    p.add_argument("--config", default="config.yaml")
    return p.parse_args()


def make_compute_metrics(task: str, processor=None):
    if task == "image-classification":
        acc_metric = evaluate.load("accuracy")
        def compute_metrics(eval_pred):
            logits, labels = eval_pred
            preds = np.argmax(logits, axis=-1)
            acc = acc_metric.compute(predictions=preds, references=labels)
            # top-5
            top5 = np.mean([
                labels[i] in np.argsort(logits[i])[-5:]
                for i in range(len(labels))
            ])
            return {"accuracy": acc["accuracy"], "top5_accuracy": top5}
        return compute_metrics

    elif task == "semantic-segmentation":
        miou_metric = evaluate.load("mean_iou")
        def compute_metrics(eval_pred):
            logits, labels = eval_pred
            preds = np.argmax(logits, axis=1)
            result = miou_metric.compute(
                predictions=preds,
                references=labels,
                num_labels=logits.shape[1],
                ignore_index=255,
                reduce_labels=False,
            )
            return {
                "mean_iou": result["mean_iou"],
                "mean_accuracy": result["mean_accuracy"],
            }
        return compute_metrics

    return None


def main():
    args = parse_args()
    with open(args.config) as f:
        cfg = yaml.safe_load(f)

    cfg["hf_token"] = os.environ.get("HF_TOKEN")

    # Populate id2label from dataset ClassLabel feature before loading model
    from datasets import load_from_disk
    _raw = load_from_disk(f"{cfg['local_data_dir']}/train")
    _label_col = "labels" if "labels" in _raw.column_names else "label"
    if hasattr(_raw.features[_label_col], "names"):
        names = _raw.features[_label_col].names
        cfg["id2label"] = {str(i): n for i, n in enumerate(names)}
        cfg["num_labels"] = len(names)
        print(f"Labels ({len(names)}): {names}")

    model, processor = load_model_and_processor(cfg)
    task = cfg["task"]

    train_ds = CVDataset(f"{cfg['local_data_dir']}/train", processor, task, is_train=True)
    eval_ds  = CVDataset(f"{cfg['local_data_dir']}/eval",  processor, task, is_train=False)

    collator = make_collate_fn_detection(processor) if task == "object-detection" else None
    compute_metrics = make_compute_metrics(task, processor)

    # Smoke-test mode: 1 step, no checkpoint write, wandb off (for Phase 5.5)
    smoke = bool(cfg.get("smoke_test", False))
    if smoke:
        os.environ["WANDB_MODE"] = "disabled"

    training_args = TrainingArguments(
        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["per_device_eval_batch_size"],
        learning_rate=cfg["learning_rate"],
        warmup_ratio=cfg.get("warmup_ratio", 0.1),
        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),
        dataloader_num_workers=cfg.get("dataloader_num_workers", 4),
        dataloader_pin_memory=cfg.get("dataloader_pin_memory", True),
        remove_unused_columns=cfg.get("remove_unused_columns", False),
        eval_strategy="no" if smoke else cfg.get("eval_strategy", "epoch"),
        save_strategy="no" if smoke else cfg.get("save_strategy", "epoch"),
        load_best_model_at_end=False if smoke else cfg.get("load_best_model_at_end", True),
        metric_for_best_model=cfg.get("metric_for_best_model", "accuracy"),
        greater_is_better=cfg.get("greater_is_better", True),
        max_steps=1 if smoke else -1,
        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"),
    )

    optimizer_tuple = (None, None)
    if task == "image-classification" and cfg.get("head_learning_rate"):
        head_prefixes = ("classifier.", "head.")
        head_params = [p for n, p in model.named_parameters() if n.startswith(head_prefixes) and p.requires_grad]
        backbone_params = [p for n, p in model.named_parameters() if not n.startswith(head_prefixes) and p.requires_grad]
        optimizer = torch.optim.AdamW(
            [
                {"params": backbone_params, "lr": cfg["learning_rate"]},
                {"params": head_params, "lr": cfg["head_learning_rate"]},
            ],
            weight_decay=cfg.get("weight_decay", 0.01),
        )
        optimizer_tuple = (optimizer, None)

    trainer = Trainer(
        model=model,
        args=training_args,
        train_dataset=train_ds,
        eval_dataset=eval_ds,
        data_collator=collator,
        compute_metrics=compute_metrics,
        optimizers=optimizer_tuple,
    )

    trainer.train()

    if smoke:
        # Find the step-level log entry. The final entry of `log_history` is the
        # training summary which has `train_loss` (not `loss`); the step entries
        # have `loss` and `grad_norm`. Searching by key avoids that confusion.
        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")
    print("Training complete. Model saved to", f"{cfg['output_dir']}/final")


if __name__ == "__main__":
    main()

run_eval.py (NOT evaluate.py — collides with HF evaluate library)

import argparse
import json
import os
import yaml
import evaluate as hf_evaluate
import numpy as np
import torch
from transformers import pipeline
from datasets import load_from_disk, Image as HFImage
from tqdm import tqdm


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 eval_classification(pipe, eval_ds, cfg):
    acc_metric = hf_evaluate.load("accuracy")
    id2label = pipe.model.config.id2label
    label2id = {v: k for k, v in id2label.items()}
    labels, preds, top5_correct = [], [], 0
    for item in tqdm(eval_ds, desc="Evaluating"):
        out = pipe(item["image"].convert("RGB"), top_k=5)
        pred_ids = [label2id.get(p["label"], 0) for p in out]
        preds.append(pred_ids[0])
        labels.append(item["label"])
        if item["label"] in pred_ids:
            top5_correct += 1
    result = acc_metric.compute(predictions=preds, references=labels)
    return {"accuracy": result["accuracy"],
            "top5_accuracy": top5_correct / len(labels),
            "n_eval": len(labels)}


def eval_detection(model, processor, eval_ds, cfg, device):
    """COCO-style mAP via torchmetrics."""
    from torchmetrics.detection import MeanAveragePrecision
    metric = MeanAveragePrecision(box_format="xyxy", iou_type="bbox")
    model.eval()
    for item in tqdm(eval_ds, desc="Evaluating"):
        image = item["image"].convert("RGB")
        w, h = image.size
        inputs = processor(images=image, return_tensors="pt").to(device)
        with torch.no_grad():
            outputs = model(**inputs)
        # Post-process to xyxy absolute coords
        results = processor.post_process_object_detection(
            outputs, target_sizes=torch.tensor([[h, w]]), threshold=0.05)[0]
        preds = [{
            "boxes": results["boxes"].cpu(),
            "scores": results["scores"].cpu(),
            "labels": results["labels"].cpu(),
        }]
        # Ground truth — convert xywh → xyxy
        gt_boxes = torch.tensor([[b[0], b[1], b[0]+b[2], b[1]+b[3]]
                                 for b in item["objects"]["bbox"]])
        gt_labels = torch.tensor(item["objects"]["category_id"])
        target = [{"boxes": gt_boxes, "labels": gt_labels}]
        metric.update(preds, target)
    out = metric.compute()
    return {
        "map_50_95": float(out["map"]),
        "map_50": float(out["map_50"]),
        "map_75": float(out["map_75"]),
        "mar_100": float(out["mar_100"]),
        "n_eval": len(eval_ds),
    }


def eval_segmentation(model, processor, eval_ds, cfg, device):
    """Mean IoU via evaluate library."""
    miou_metric = hf_evaluate.load("mean_iou")
    model.eval()
    num_labels = model.config.num_labels
    for item in tqdm(eval_ds, desc="Evaluating"):
        image = item["image"].convert("RGB")
        gt_mask = item.get("annotation") or item.get("mask")
        gt_arr = np.array(gt_mask)
        inputs = processor(images=image, return_tensors="pt").to(device)
        with torch.no_grad():
            outputs = model(**inputs)
        pred = processor.post_process_semantic_segmentation(
            outputs, target_sizes=[gt_arr.shape[-2:]])[0]
        miou_metric.add(predictions=pred.cpu().numpy(), references=gt_arr)
    result = miou_metric.compute(num_labels=num_labels, ignore_index=255, reduce_labels=False)
    return {
        "mean_iou": float(result["mean_iou"]),
        "mean_accuracy": float(result["mean_accuracy"]),
        "overall_accuracy": float(result["overall_accuracy"]),
        "n_eval": len(eval_ds),
    }


def eval_depth(model, processor, eval_ds, cfg, device):
    """AbsRel, RMSE, δ<1.25."""
    model.eval()
    abs_rels, rmses, deltas = [], [], []
    for item in tqdm(eval_ds, desc="Evaluating"):
        image = item["image"].convert("RGB")
        gt = np.array(item.get("depth") or item.get("depth_map"), dtype=np.float32)
        inputs = processor(images=image, return_tensors="pt").to(device)
        with torch.no_grad():
            outputs = model(**inputs)
        pred = outputs.predicted_depth.squeeze().cpu().numpy()
        # Resize pred to gt shape if needed
        if pred.shape != gt.shape:
            from PIL import Image as PILImage
            pred = np.array(PILImage.fromarray(pred).resize(gt.shape[::-1]))
        valid = gt > 0
        if not valid.any():
            continue
        abs_rel = np.abs(pred[valid] - gt[valid]) / gt[valid]
        rmse = np.sqrt(((pred[valid] - gt[valid])**2).mean())
        ratio = np.maximum(pred[valid] / gt[valid], gt[valid] / pred[valid])
        abs_rels.append(abs_rel.mean()); rmses.append(rmse); deltas.append((ratio < 1.25).mean())
    return {
        "abs_rel": float(np.mean(abs_rels)),
        "rmse": float(np.mean(rmses)),
        "delta_1.25": float(np.mean(deltas)),
        "n_eval": len(eval_ds),
    }


def main():
    args = parse_args()
    with open(args.config) as f:
        cfg = yaml.safe_load(f)

    task = cfg["task"]
    hf_task_map = {
        "image-classification": "image-classification",
        "object-detection": "object-detection",
        "semantic-segmentation": "image-segmentation",
        "depth-estimation": "depth-estimation",
    }

    device = "cuda" if torch.cuda.is_available() else "cpu"

    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())

    # Load checkpoints in float32 for eval/inference. Training with `bf16=True`
    # writes checkpoints whose weights are bfloat16; image processors emit
    # float32 pixel_values. Loading the checkpoint with `torch_dtype=bfloat16`
    # causes "Input type (float) and bias type (BFloat16) should be the same"
    # at the first conv. float32 is safe for inference of any CV checkpoint.
    eval_dtype = torch.float32
    if task == "image-classification":
        pipe = pipeline("image-classification", model=args.checkpoint, device=0 if device == "cuda" else -1,
                        torch_dtype=eval_dtype, token=os.environ.get("HF_TOKEN"))
        results = eval_classification(pipe, eval_ds, cfg)
    elif task == "object-detection":
        from transformers import AutoImageProcessor, AutoModelForObjectDetection
        processor = AutoImageProcessor.from_pretrained(args.checkpoint, token=os.environ.get("HF_TOKEN"))
        model = AutoModelForObjectDetection.from_pretrained(
            args.checkpoint, torch_dtype=eval_dtype, token=os.environ.get("HF_TOKEN")).to(device)
        results = eval_detection(model, processor, eval_ds, cfg, device)
    elif task == "semantic-segmentation":
        from transformers import AutoImageProcessor, AutoModelForSemanticSegmentation
        processor = AutoImageProcessor.from_pretrained(args.checkpoint, token=os.environ.get("HF_TOKEN"))
        model = AutoModelForSemanticSegmentation.from_pretrained(
            args.checkpoint, torch_dtype=eval_dtype, token=os.environ.get("HF_TOKEN")).to(device)
        results = eval_segmentation(model, processor, eval_ds, cfg, device)
    elif task == "depth-estimation":
        from transformers import AutoImageProcessor, AutoModelForDepthEstimation
        processor = AutoImageProcessor.from_pretrained(args.checkpoint, token=os.environ.get("HF_TOKEN"))
        model = AutoModelForDepthEstimation.from_pretrained(
            args.checkpoint, torch_dtype=eval_dtype, token=os.environ.get("HF_TOKEN")).to(device)
        results = eval_depth(model, processor, eval_ds, cfg, device)
    else:
        raise ValueError(f"Unknown task: {task}")

    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))


if __name__ == "__main__":
    main()

inference.py

import argparse
import json
import os
import yaml
import torch
from PIL import Image, ImageDraw, ImageFont
from pathlib import Path
from transformers import pipeline


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 draw_detection(image, predictions):
    draw = ImageDraw.Draw(image)
    for pred in predictions:
        box = pred["box"]
        label = pred["label"]
        score = pred["score"]
        x1, y1, x2, y2 = box["xmin"], box["ymin"], box["xmax"], box["ymax"]
        draw.rectangle([x1, y1, x2, y2], outline="red", width=3)
        draw.text((x1, y1 - 15), f"{label} {score:.2f}", fill="red")
    return image


def main():
    args = parse_args()
    with open(args.config) as f:
        cfg = yaml.safe_load(f)

    task = cfg["task"]
    hf_task_map = {
        "image-classification": "image-classification",
        "object-detection": "object-detection",
        "semantic-segmentation": "image-segmentation",
        "depth-estimation": "depth-estimation",
    }

    # Load in float32: checkpoints trained with bf16=True save bfloat16 weights,
    # but image processors emit float32 pixel_values. Loading the model in
    # bfloat16 produces a dtype-mismatch crash on the first conv layer.
    pipe = pipeline(
        hf_task_map[task],
        model=args.checkpoint,
        device=0 if torch.cuda.is_available() else -1,
        torch_dtype=torch.float32,
        token=os.environ.get("HF_TOKEN"),
    )

    from datasets import load_from_disk, Image as HFImage
    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")
        prediction = pipe(image)

        image.save(out_dir / f"sample_{i}_input.jpg")

        if task == "image-classification":
            top_label = prediction[0]["label"]
            top_score = prediction[0]["score"]
            draw = ImageDraw.Draw(image)
            draw.text((10, 10), f"{top_label}: {top_score:.3f}", fill="white")
            meta = {"top_predictions": prediction[:5]}

        elif task == "object-detection":
            image = draw_detection(image, prediction)
            meta = {"detections": prediction}

        elif task == "semantic-segmentation":
            # Overlay mask on image
            meta = {"segments": [{"label": s["label"], "score": s["score"]} for s in prediction]}

        elif task == "depth-estimation":
            depth_map = prediction["predicted_depth"]
            meta = {"depth_shape": list(depth_map.shape) if hasattr(depth_map, "shape") else "N/A"}

        image.save(out_dir / f"sample_{i}_pred.jpg")
        with open(out_dir / f"sample_{i}_meta.json", "w") as f:
            json.dump(meta, f, indent=2, default=str)

        print(f"Sample {i}: {meta}")


if __name__ == "__main__":
    main()

Detection-Specific Gotchas

Most of the gotchas below are already pre-fixed in the templates above. Listed here for documentation and so smoke-test failures can be traced to them quickly. Do not "re-fix" them in generated code.

HANDLED: variable-sized pixel_values in the batch Most detection processors resize per sample, so torch.stack fails with stack expects equal size. make_collate_fn_detection(processor) does a manual torch.nn.functional.pad to the max H, W in the batch and constructs the cross-attention pixel_mask. Version-agnostic across transformers 4.x and 5.x (5.x's processor.pad was rewritten as a per-image API and no longer takes return_tensors).

HANDLED: dtype mismatch at eval/inference Trainer with bf16=True saves bfloat16 weights; image processors emit float32 pixel_values. run_eval.py and inference.py load with torch_dtype=float32 to keep the conv input dtype consistent.

HANDLED: per-sample label dict has no batch dim The processor returns labels as a list-of-1 dict whose tensors lack a batch dim (e.g. shape (1,) for class_labels). Squeezing the dict uniformly turns those into 0-dim tensors and crashes loss compute. The template extracts inputs["labels"][0] and only squeezes pixel_values.

HANDLED: timm backbone import Several detection models (DETR, Conditional/Deformable DETR, etc.) use timm ResNet backbones by default. timm is in requirements.txt. (No need to set revision="no_timm" — once timm is installed, the default branch works.)

GOTCHA: Detection datasets need remove_unused_columns=False Detection labels are dicts of variable-length tensors. The default Trainer behavior strips unrecognized columns and breaks the labels. Always set:

TrainingArguments(remove_unused_columns=False, ...)

GOTCHA: bbox format — xywh vs xyxy Different datasets use different formats. DETR processor expects xywh (COCO format). If your dataset uses xyxy, convert in dataset.py:

# xyxy → xywh
x1, y1, x2, y2 = bbox
bbox_xywh = [x1, y1, x2 - x1, y2 - y1]

GOTCHA: dataloader_pin_memory=False for detection Detection collators use variable-length labels (list of dicts). Pin memory requires uniform tensors. Set dataloader_pin_memory: false in config.yaml for detection tasks.

Recommended detection models (small, fast to train):

  • ustc-community/dfine-small-coco — D-FINE small, COCO pretrained, 10.4M params

  • hustvl/yolos-tiny — YOLOS tiny, 6.5M params, fast inference

  • facebook/detr-resnet-50 — classic DETR, widely tested

Recommended segmentation models:

  • nvidia/segformer-b0-finetuned-ade-512-512 — SegFormer B0, 3.7M params

  • nvidia/segformer-b2-finetuned-cityscapes-1024-1024 — B2, 24.7M params

Recommended classification models:

  • google/vit-base-patch16-224 — ViT-Base, 86M params, strong baseline

  • facebook/convnext-tiny-224 — ConvNeXt-Tiny, 28M params, efficient

  • microsoft/swin-tiny-patch4-window7-224 — Swin-Tiny, 28M params


Per-Task Metrics Summary

TaskPrimaryEval libraryNotes
classificationtop-1 accuracyevaluate.load("accuracy")Also track top-5
object-detection[email protected]:0.95torchmetrics.detection.MeanAveragePrecisionCOCO-style
semantic-segmean IoUevaluate.load("mean_iou")ignore_index=255
instance-segmask APtorchmetricspanoptic quality optional
depth-estimationAbsRelmanualAlso RMSE, δ<1.25

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