Generated banner for Nv Segment Ct Finetune
Generated logo for Nv Segment Ct Finetune

Nv Segment Ct Finetune

NVIDIAOfficialCreated by NVIDIA MedTech

  • Skill
  • Workflow
  • Data and AI
  • Apache-2.0
  • Medium risk
  • Has scripts

From the author

Runs standard or fixed-channel softmax finetuning of NV-Segment-CT VISTA3D on CT NIfTI image/label datasets, with optional MONAI-native MLflow tracking and checkpoint evidence. Uses softmax for predefined, mutually exclusive classes; keeps the standard workflow when point prompts or runtime-variable classes are needed. Not for clinical validation.
— the description by NVIDIA MedTech

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

This skill provides a structured workflow for fine-tuning the NV-Segment-CT VISTA3D model on CT NIfTI image and label datasets. It supports two primary training modes: a standard continual-learning workflow and a fixed-channel softmax workflow for mutually exclusive class segmentation. The skill includes automated configuration generation, optional MLflow tracking for experiment management, and sanity-check presets for reproducing MSD Task06 Lung Tumor results. It is designed for research and development purposes and is not intended for clinical validation or autonomous diagnosis.

What it does

  • Fine-tunes VISTA3D models on custom CT NIfTI datasets.
  • Supports fixed-channel softmax training for mutually exclusive labels.
  • Provides a sanity-check preset for MSD Task06 Lung Tumor reproduction.
  • Integrates with MLflow for experiment tracking and metric logging.
  • Generates structured output including performance metrics and checkpoint recommendations.
  • Validates input datasets and provides audit reports.

When to use it

  • When you need to adapt a pre-trained VISTA3D model to a specific CT anatomy.
  • When labels are mutually exclusive and require a fixed-channel softmax output.
  • When reproducing standard benchmark results for validation purposes.
  • When tracking training progress and metrics via MLflow.

Advantages

  • Automates complex bundle configuration and environment setup.
  • Provides clear guidance on choosing between standard and softmax workflows.
  • Includes built-in sanity checks to verify environment and model performance.
  • Standardizes output reporting for easier model evaluation.

Usage examples

“Fine-tune the model on my lung tumor dataset using the standard workflow.”
The agent executes the fine-tuning script with the provided dataset, target anatomy, and default settings, outputting a recommended checkpoint and performance metrics.
“Run a softmax fine-tuning for my mutually exclusive labels.”
The agent configures the training for fixed-channel output using the specified label mapping and initiates the training process.
“Perform a sanity check using the Task06 Lung Tumor dataset.”
The agent runs the predefined sanity preset, comparing results against reference scores to verify the environment and model behavior.
“Fine-tune with MLflow tracking enabled.”
The agent initiates the training process and logs metrics to the specified MLflow experiment.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/nvidia/nv-segment-ct-finetune/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip nv-segment-ct-finetune.zip -d .agents/skills

Documentation by NVIDIA MedTech

NV-Segment-CT Finetune

Purpose

  • Used for smoke or dataset finetuning of NV-Segment-CT VISTA3D on CT NIfTI labels, including the upstream fixed-channel softmax workflow and optional MLflow tracking. Not for clinical validation.

  • Wraps the upstream MONAI bundle entrypoint; do not replace it with handwritten training or inference code.

  • Manifest inputs are dataset_dir, datalist, target_anatomy, label_mapping, smoke, sanity, auto_seg, softmax, skip_formal_eval, mlflow_tracking_uri, mlflow_experiment_name, and mlflow_run_name.

  • Manifest outputs are finetuned_ckpt and schema-checked result_json.

Instructions

  • Run only the caller-requested preset, dataset, output directory, and compute budget. Dependency setup and remote tracking require the caller's approval. After reporting the result, stop; further training, publishing, or deployment is a separate request.

  • Run scripts/run_finetune.py; do not patch files under bundle/ or upstream checkouts during normal skill use.

  • For standalone Bash, include the fresh-environment setup line before the wrapper; benchmark venvs start empty.

  • Run the committed script in place from the repo root. Do not copy this skill to a runtime directory, and do not use rm or cleanup commands in generated invocations.

  • If a host exposes run_script, use run_script("scripts/run_finetune.py", args=[...]); otherwise run from the repo root.

  • For the shortest workflow check, use --smoke; for MSD Task06 Lung Tumor reproduction, use --sanity.

  • Choose between the standard and --softmax workflows using the criteria below. Do not combine --softmax with --auto-seg or --sanity.

  • Set --mlflow-experiment-name to enable MLflow for the training phase of either workflow. --mlflow-tracking-uri and --mlflow-run-name require an experiment name. Formal pre/post evaluation does not receive MLflow credentials.

  • Read references/task06-and-results.md only when you need Task06 reference details, output-field definitions, or manual bundle setup notes.

Choosing the Workflow

Use --softmax only when all of these conditions hold:

  • The complete class set is known before training and will not vary between inference requests.

  • Labels are mutually exclusive: each voxel is background or exactly one foreground class.

  • Every foreground dataset label maps to an existing VISTA3D class ID, and a conventional fixed-channel output is desired.

Keep the standard workflow if point prompts must remain available, classes are selected dynamically at inference, labels can overlap, or the Task06 --sanity reproduction is required.

For --label-mapping '[[1,3],[2,13]]', channel 0 is background, channel 1 represents dataset label 1 initialized from VISTA3D class 3, and channel 2 represents dataset label 2 initialized from VISTA3D class 13. Preserve the entries and their order when using the resulting model_softmax.pt with upstream configs/inference_softmax.json. The nv-segment-ct and nv-segment-ctmr inference skills do not currently expose that fixed-channel inference path.

Available Scripts

ScriptPurposeArguments
scripts/run_finetune.pyPrimary entrypoint declared by skill_manifest.yaml; stages configs, runs MONAI, and writes output.json.[FIXTURE_OR_DATASET] --output-dir OUT_DIR [--smoke] [--sanity] [--auto-seg] [--softmax] [--dataset-dir DIR] [--datalist JSON] [--target-anatomy TEXT] [--label-mapping JSON] [--patch-size JSON] [--mlflow-experiment-name NAME] [--mlflow-tracking-uri URI] [--mlflow-run-name NAME]

Prerequisites

  • Python 3.10+ with CUDA-capable Torch for GPU runs.

  • Runtime packages from skill_manifest.yaml, especially monai==1.4.0, numpy<2, nibabel, scipy, typer, PyYAML, fire, pytorch-ignite, einops, and huggingface_hub. Install mlflow>=2.10,<4 when MLflow tracking is enabled.

  • Optional environment variables: CUDA_VISIBLE_DEVICES restricts visible GPUs; NPROC_PER_NODE overrides GPU count and values >=2 select multi-GPU mode for non-sanity runs; NVSEG_FINETUNE_AUTO_VENV=0 disables the cached MONAI 1.4 compatibility environment. Remote tracking may use DATABRICKS_CONFIG_PROFILE, DATABRICKS_HOST, DATABRICKS_TOKEN, MLFLOW_TRACKING_CLIENT_CERT_PATH, MLFLOW_TRACKING_INSECURE_TLS, MLFLOW_TRACKING_PASSWORD, MLFLOW_TRACKING_SERVER_CERT_PATH, MLFLOW_TRACKING_TOKEN, or MLFLOW_TRACKING_USERNAME; these variables are forwarded only when MLflow is explicitly enabled, and unrelated credentials are not forwarded.

  • --softmax also needs the pinned NVIDIA-Medtech source checkout. Set NV_SEGMENT_CT_ROOT to its NV-Segment-CT directory, or set NV_SEGMENT_CTMR_ROOT to the sibling NV-Segment-CTMR directory. The wrapper reads the official softmax config and implementation in place and writes generated overrides only under --output-dir.

  • Run outputs: generated bundle configs under skills/nv-segment-ct-finetune/bundle/configs/, including auto_override.json, train_continual_task06_lung.json, and dfw_no_logging.json; checkpoints/evidence under --output-dir; and local tracking data under <output-dir>/mlruns when enabled.

  • Dependency cache locations: ~/.cache/nvidia-skills/venvs/nv-segment-ct-finetune-monai14/ for MONAI compatibility packages and ~/.cache/huggingface/ for model assets. These are reusable runtime files, not agent instructions or authorization for another run. Set NVSEG_FINETUNE_AUTO_VENV=0 when compatibility-environment setup is not approved; then use a caller-provided compatible environment.

  • Network access: model/config downloads use https://huggingface.co and https://raw.githubusercontent.com; remote tracking contacts only the caller-approved MLflow or Databricks destination when explicitly enabled. The label-dictionary download accepts HTTPS on the pinned source host and rejects redirects.

Fresh environment setup:

python -m pip install "monai==1.4.0" "numpy<2" pytorch-ignite einops nibabel scipy typer PyYAML fire huggingface_hub

When MLflow tracking is enabled, also install:

python -m pip install "mlflow>=2.10,<4"

Known upstream compatibility constraints:

  • DFW Task06 reference: Python 3.10.16, MONAI 1.4.0, Torch 2.7.0+cu126.

  • Use exact monai==1.4.0 for smoke, sanity, and evidence runs; MONAI 1.5.x can crash the upstream finetune loss on boolean labels.

  • Do not float the dependency as monai>=1.4,<1.6 in generated commands.

  • The softmax workflow keeps the upstream defaults of 100 epochs and learning rate 1e-4 unless the caller overrides them.

One-time source setup for --softmax:

export NV_SEGMENT_CTMR_COMMIT=cb921f5c58837c0f42a713855d68b32af88e1cdd
export NV_SEGMENT_CTMR_CHECKOUT="$HOME/.cache/nvidia-skills/upstreams/NV-Segment-CTMR-cb921f5"
if [ ! -d "$NV_SEGMENT_CTMR_CHECKOUT/.git" ]; then
  git clone https://github.com/NVIDIA-Medtech/NV-Segment-CTMR.git "$NV_SEGMENT_CTMR_CHECKOUT"
fi
git -C "$NV_SEGMENT_CTMR_CHECKOUT" checkout --detach "$NV_SEGMENT_CTMR_COMMIT"
export NV_SEGMENT_CT_ROOT="$NV_SEGMENT_CTMR_CHECKOUT/NV-Segment-CT"

Usage

Smoke-scale workflow check:

python -m pip install "monai==1.4.0" "numpy<2" pytorch-ignite einops nibabel scipy typer PyYAML fire huggingface_hub && \
python skills/nv-segment-ct-finetune/scripts/run_finetune.py \
  PATH_TO_DATASET \
  --smoke \
  --patch-size '[64,64,64]' \
  --output-dir runs/nvseg_smoke

Use the staged dataset as PATH_TO_DATASET. For the micro fixture, use skills/nv-segment-ct-finetune/fixtures/spleen_micro. Smoke mode proves wiring, config generation, checkpoint loading, and runtime compatibility; it is not a quality bar.

MSD Task06 Lung Tumor sanity reproduction:

python skills/nv-segment-ct-finetune/scripts/run_finetune.py \
  /path/to/Task06 \
  --sanity \
  --output-dir runs/nvseg_task06_sanity

The sanity preset follows the single-GPU DFW recipe: fold-0 validation, label mapping [[1, 23]] for lung tumor, automatic class-prompt segmentation, patch [128,128,128], 5 epochs, and original-spacing configs/evaluate.json scoring before and after training. Expected reference range is pretrained Dice about 0.6697, training-best Dice about 0.6905, and fine-tuned formal Dice about 0.6836.

User-data finetune:

python skills/nv-segment-ct-finetune/scripts/run_finetune.py \
  --dataset-dir /path/to/dataset \
  --datalist /path/to/datalist.json \
  --target-anatomy "lung tumor" \
  --auto-seg \
  --epochs 5 \
  --patch-size '[128,128,128]' \
  --output-dir runs/nvseg_user_finetune

Use --label-mapping '[[1, 23]]' when local label values are custom or the anatomy name is ambiguous.

Optional local MLflow tracking:

python skills/nv-segment-ct-finetune/scripts/run_finetune.py \
  --dataset-dir /path/to/dataset \
  --datalist /path/to/datalist.json \
  --target-anatomy "lung tumor" \
  --epochs 5 \
  --mlflow-experiment-name nvseg-finetune \
  --mlflow-run-name trial-01 \
  --output-dir runs/nvseg_mlflow

This uses MONAI's documented --tracking mlflow path and built-in rank-zero handlers. With no --mlflow-tracking-uri, data stays in <output-dir>/mlruns. Pass a caller-approved remote URI, including databricks, only when remote tracking is intended. MLflow does not change patch size, transforms, optimizer values, DataLoader settings, or other training configuration.

Fixed-channel softmax finetune for mutually exclusive labels:

export NV_SEGMENT_CT_ROOT="$HOME/.cache/nvidia-skills/upstreams/NV-Segment-CTMR-cb921f5/NV-Segment-CT"
python skills/nv-segment-ct-finetune/scripts/run_finetune.py \
  --dataset-dir /path/to/dataset \
  --datalist /path/to/datalist.json \
  --label-mapping '[[1,3],[2,13]]' \
  --softmax \
  --epochs 100 \
  --output-dir runs/nvseg_softmax

This delegates to upstream configs/train_continual_softmax.json. It produces checkpoints/model_softmax.pt; the source model.pt initializes the network but is not compatible with configs/inference_softmax.json. The wrapper therefore recommends the produced softmax checkpoint after a successful run.

Examples

Smoke run on a staged tiny dataset:

python skills/nv-segment-ct-finetune/scripts/run_finetune.py \
  runs/with_vs_without_nv/_inputs/nv_segment_ct_finetune/input_dataset \
  --smoke \
  --patch-size '[64,64,64]' \
  --output-dir runs/nvseg_smoke

Task06 sanity run on a local MSD cache:

python skills/nv-segment-ct-finetune/scripts/run_finetune.py \
  .workbench_data/datasets/Task06_Lung \
  --sanity \
  --output-dir runs/nvseg_task06_sanity

Data Contract

  • Preferred layout: dataset/imagesTr/*.nii.gz and dataset/labelsTr/*.nii.gz.

  • Labels must align one-to-one with images by basename.

  • The target label value must be present in the training labels.

  • Use a datalist when patient-level splitting matters. The bundle default fold is 0, so fold: 0 entries are validation and all other folds are training.

  • Every trained foreground label must map to an existing VISTA3D global class id from bundle/label_dict.json; this skill cannot invent a new class.

  • In --softmax mode, the first mapping column is the saved dataset label and the second is the pretrained VISTA class ID. Mapping order fixes the channel layout and must remain unchanged during inference.

Results

Check output.json in the run directory first:

  • formal_pretrained_val_dice and formal_finetuned_val_dice: original-spacing pre/post scores when formal eval is enabled.

  • training_start_val_dice, val_dice_per_epoch, and training_best_val_dice: training-time validation trace.

  • finetuned_ckpt_matches_pretrained_weights: detects the standard workflow's epoch-0 checkpoint trap when val_at_start=true; softmax uses a different checkpoint architecture.

  • recommended_ckpt: checkpoint recommendation derived from the recorded workflow and metrics. Inspect those records before selecting a checkpoint; the last epoch or a filename alone is not evidence of improvement. Report the recommendation to the caller; deployment is outside this skill's scope.

  • invocation.mlflow_tracking: selected tracking URI, experiment name, and optional run name, or null when tracking was disabled.

  • runtime.oom, runtime.peak_gpu_mb, and phase logs: distinguish OOM, slow validation, and process failure.

Decision rule: prefer formal original-spacing pre/post scores when present; reject tensor-identical "fine-tuned" checkpoints for sanity recovery; treat improved: false as valid evidence rather than a wrapper failure.

Limitations

  • Thin wrapper. Training, validation, transforms, and checkpointing are delegated to the upstream bundle in bundle/.

  • Tensor comparison uses restricted weights_only=True checkpoint loading. Unsupported serialized objects produce a comparison error; they are not retried with unrestricted pickle loading. This comparison is not a security audit of the upstream training/checkpoint loader.

  • Reproduction record only: the successful five-epoch Task06 run used Python 3.12.3, PyTorch 2.12.0+cu130 with CUDA 13.0, MONAI 1.4.0, NumPy 1.26.4, PyTorch-Ignite 0.5.4, NiBabel 5.4.2, SciPy 1.16.0, einops 0.8.2, Fire 0.7.1, Hugging Face Hub 0.36.2, Transformers 4.57.6, Typer 0.25.1, PyYAML 6.0.3, and MLflow 3.14.0 on one NVIDIA RTX 6000 Ada 48 GB GPU. These versions document the evidence environment; they are not additional package constraints or a claim that other versions cannot work.

  • The auto-derived plan is heuristic; caller-provided --patch-size, --cache-rate, --epochs, and --learning-rate win.

  • --softmax is not compatible with --sanity: the Task06 reference scores and original-spacing pre/post evaluation belong to the standard VISTA3D continual-learning workflow. Softmax runs record the training validation trajectory but need a separate task-specific evaluation before quality claims.

  • The Task06 sanity recipe intentionally forces single-GPU execution to match the DFW reference. Multi-GPU mode for other datasets requires host torchrun support.

  • The paired verifier is CPU-only and audits the evidence pack; it does not re-run GPU segmentation.

  • MLflow support is optional and uses MONAI's built-in tracking handlers. Tracking errors are part of the upstream MONAI run and can therefore fail the finetune command.

  • Not for clinical deployment, clinical interpretation, autonomous diagnosis, or regulatory submission.

Troubleshooting

ErrorCauseFix
Missing dependency or import errorRuntime drift from skill_manifest.yaml.Install the packages above or use the documented environment.
Low Task06 pretrained DiceWrong config, wrong checkpoint, data split drift, or dependency drift.Compare environment fields and staged configs before changing training logic.
model_finetune.pt matches pretrainedval_at_start=true selected epoch 0 as best.Use recommended_ckpt; treat sanity recovery as failed unless a changed checkpoint improves formal Dice.
Missing formal Dice fieldsFormal eval failed or was skipped.Inspect eval_pretrained.log, eval_finetuned.log, and metrics.csv.
GPU out of memoryPatch/cache settings too large.Reduce --patch-size, lower --cache-rate, or reduce workers.
No validation casesDatalist lacks fold: 0.Provide at least one validation entry.
--softmax requires the pinned ... checkoutThe August softmax config/implementation is absent or the checkout is at a different commit.Check out cb921f5c58837c0f42a713855d68b32af88e1cdd and set NV_SEGMENT_CT_ROOT or NV_SEGMENT_CTMR_ROOT.
MLflow tracking failsMLflow is absent, credentials are invalid, or the experiment is inaccessible.Inspect finetune.log, fix the MLflow client configuration, and rerun; omit --mlflow-experiment-name to disable tracking.

Verification

Run the implemented verifier when quality gates matter:

python -m eval_engine.run_trusted skills/nv-segment-ct-finetune \
  --fixture skills/nv-segment-ct-finetune/fixtures/spleen_micro \
  --out runs/nvseg_trusted

Files

tests/test_run_finetune.py

# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# 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.

import importlib.util
import io
from pathlib import Path
from types import SimpleNamespace

import pytest

SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "run_finetune.py"
spec = importlib.util.spec_from_file_location("run_finetune", SCRIPT)
mod = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(mod)


def test_download_label_dict_uses_reviewed_https_origin(tmp_path, monkeypatch):
    calls = []

    def open_source(url, *, timeout):
        calls.append((url, timeout))
        return io.BytesIO(b'{"lung tumor": 23}\n')

    def build_opener(handler):
        assert isinstance(handler, mod._NoLabelDictRedirects)
        return SimpleNamespace(open=open_source)

    monkeypatch.setattr(mod.urllib.request, "build_opener", build_opener)
    dst = tmp_path / "bundle" / "label_dict.json"

    assert mod._download_label_dict(dst) is True
    assert calls == [(mod.LABEL_DICT_URL, 30)]
    assert mod.json.loads(dst.read_text()) == {"lung tumor": 23}
    # A cache hit must not issue a second request or overwrite the saved file.
    assert mod._download_label_dict(dst) is False
    assert len(calls) == 1


@pytest.mark.parametrize(
    "url",
    [
        "http://raw.githubusercontent.com/labels.json",
        "file:///tmp/labels.json",
        "ftp://raw.githubusercontent.com/labels.json",
        "custom://raw.githubusercontent.com/labels.json",
        "https://example.invalid/labels.json",
        "https://raw.githubusercontent.com.example.invalid/labels.json",
        "https://[email protected]/labels.json",
        "https://raw.githubusercontent.com:80/labels.json",
        "https://raw.githubusercontent.com:invalid/labels.json",
        "https://raw.githubusercontent.com/labels.json#fragment",
    ],
)
def test_download_label_dict_rejects_other_origins_before_io(tmp_path, monkeypatch, url):
    def unexpected_opener(*args, **kwargs):
        pytest.fail("invalid download source must be rejected before opening a connection")

    monkeypatch.setattr(mod, "LABEL_DICT_URL", url)
    monkeypatch.setattr(mod.urllib.request, "build_opener", unexpected_opener)
    dst = tmp_path / "not-created" / "label_dict.json"

    assert mod._download_label_dict(dst) is False
    assert not dst.parent.exists()


@pytest.mark.parametrize(
    "newurl",
    [
        "http://raw.githubusercontent.com/labels.json",
        "https://example.invalid/labels.json",
        "file:///tmp/labels.json",
        "https://raw.githubusercontent.com/other.json",
    ],
)
def test_label_dict_redirect_handler_rejects_all_redirects(newurl):
    handler = mod._NoLabelDictRedirects()
    request = mod.urllib.request.Request(mod.LABEL_DICT_URL)
    assert handler.redirect_request(request, None, 302, "Found", {}, newurl) is None


def test_download_label_dict_handles_rejected_redirect(tmp_path, monkeypatch):
    def open_source(url, *, timeout):
        raise mod.urllib.error.HTTPError(url, 302, "redirect rejected", {}, None)

    monkeypatch.setattr(
        mod.urllib.request, "build_opener", lambda _: SimpleNamespace(open=open_source)
    )
    dst = tmp_path / "bundle" / "label_dict.json"
    assert mod._download_label_dict(dst) is False
    assert not dst.exists()


def test_prepare_bundle_files_stages_train_configs_from_local_upstream(tmp_path, monkeypatch):
    bundle = tmp_path / "skill" / "bundle"
    upstream_configs = (
        tmp_path / ".workbench_data" / "upstreams" / "NV-Segment-CTMR" / "NV-Segment-CT" / "configs"
    )
    upstream_configs.mkdir(parents=True)
    for name in (
        "train.json",
        "train_continual.json",
        "multi_gpu_train.json",
        "evaluate.json",
    ):
        (upstream_configs / name).write_text(f'{{"name": "{name}"}}\n')
    (bundle / "configs").mkdir(parents=True)
    (bundle / "metadata.json").write_text("{}\n")
    (bundle / "vista3d_pretrained_model").mkdir(parents=True)
    (bundle / "vista3d_pretrained_model" / "model.pt").write_bytes(b"model")
    (bundle / "label_dict.json").write_text('{"lung tumor": 23}\n')

    monkeypatch.setattr(mod, "BUNDLE_DIR", bundle)
    monkeypatch.setattr(mod, "SKILL_DIR", tmp_path / "skill")
    monkeypatch.setattr(mod, "_REPO_ROOT", tmp_path)
    monkeypatch.setattr(mod, "LABEL_DICT", bundle / "label_dict.json")
    notes = mod.prepare_bundle_files()

    for name in (
        "train.json",
        "train_continual.json",
        "multi_gpu_train.json",
        "evaluate.json",
    ):
        assert (bundle / "configs" / name).read_text() == f'{{"name": "{name}"}}\n'
    assert (bundle / "configs" / "metadata.json").is_file()
    assert (bundle / "models" / "model.pt").is_file()
    assert "restored configs/train.json from local upstream cache" in notes


def test_prepare_bundle_files_restores_drifted_train_configs(tmp_path, monkeypatch):
    bundle = tmp_path / "skill" / "bundle"
    upstream_configs = (
        tmp_path / ".workbench_data" / "upstreams" / "NV-Segment-CTMR" / "NV-Segment-CT" / "configs"
    )
    upstream_configs.mkdir(parents=True)
    for name in (
        "train.json",
        "train_continual.json",
        "multi_gpu_train.json",
        "evaluate.json",
    ):
        (upstream_configs / name).write_text(f'{{"canonical": "{name}"}}\n')
    (bundle / "configs").mkdir(parents=True)
    for name in (
        "train.json",
        "train_continual.json",
        "multi_gpu_train.json",
        "evaluate.json",
    ):
        (bundle / "configs" / name).write_text(f'{{"drifted": "{name}"}}\n')
    (bundle / "metadata.json").write_text("{}\n")
    (bundle / "vista3d_pretrained_model").mkdir(parents=True)
    (bundle / "vista3d_pretrained_model" / "model.pt").write_bytes(b"model")
    (bundle / "label_dict.json").write_text('{"lung tumor": 23}\n')

    monkeypatch.setattr(mod, "BUNDLE_DIR", bundle)
    monkeypatch.setattr(mod, "SKILL_DIR", tmp_path / "skill")
    monkeypatch.setattr(mod, "_REPO_ROOT", tmp_path)
    monkeypatch.setattr(mod, "LABEL_DICT", bundle / "label_dict.json")
    notes = mod.prepare_bundle_files()

    assert (bundle / "configs" / "evaluate.json").read_text() == '{"canonical": "evaluate.json"}\n'
    assert "restored configs/evaluate.json from local upstream cache" in notes


def test_upstream_config_dirs_accept_explicit_local_checkout(tmp_path, monkeypatch):
    config_dir = (
        tmp_path / ".workbench_data" / "upstreams" / "NV-Segment-CTMR" / "NV-Segment-CT" / "configs"
    )
    config_dir.mkdir(parents=True)
    monkeypatch.setattr(mod, "_REPO_ROOT", tmp_path)

    assert config_dir in mod._upstream_config_dirs()


def test_resolve_softmax_bundle_root_accepts_pinned_layout(tmp_path, monkeypatch):
    bundle_root = tmp_path / "NV-Segment-CT"
    (bundle_root / "configs").mkdir(parents=True)
    (bundle_root / "scripts").mkdir()
    for name in ("train_continual_softmax.json", "inference_softmax.json"):
        (bundle_root / "configs" / name).write_text("{}\n")
    (bundle_root / "scripts" / "vista3d_softmax.py").write_text("class Vista3dSoftmax: pass\n")

    monkeypatch.setenv("NV_SEGMENT_CT_ROOT", str(bundle_root))
    monkeypatch.delenv("NV_SEGMENT_CTMR_ROOT", raising=False)

    assert mod.resolve_softmax_bundle_root() == bundle_root


def test_build_override_defines_bundle_image_and_label_keys(tmp_path):
    override = mod.build_override(
        tmp_path / "dataset",
        tmp_path / "datalist.json",
        {"default": [[1, 3]]},
        [64, 64, 64],
        1.0,
        2,
        5e-5,
        tmp_path / "checkpoints",
        tmp_path / "val_during_train",
    )

    assert override["image_key"] == "image"
    assert override["label_key"] == "label"


def test_build_override_auto_seg_matches_task06_prompt_settings(tmp_path):
    override = mod.build_override(
        tmp_path / "dataset",
        tmp_path / "datalist.json",
        {"default": [[1, 23]]},
        [128, 128, 128],
        1.0,
        5,
        5e-5,
        tmp_path / "checkpoints",
        tmp_path / "val_during_train",
        auto_seg=True,
    )

    assert override["drop_label_prob"] == 0.0
    assert override["drop_point_prob"] == 1.0
    expected_spacing = tuple(float("1.5") for _ in range(3))
    assert override["resample_to_spacing"] == expected_spacing


def test_build_softmax_override_uses_fixed_channel_contract(tmp_path, monkeypatch):
    bundle = tmp_path / "bundle"
    monkeypatch.setattr(mod, "BUNDLE_DIR", bundle)

    override = mod.build_softmax_override(
        tmp_path / "dataset",
        tmp_path / "datalist.json",
        {"default": [[1, 3], [2, 13]]},
        [128, 128, 128],
        0.5,
        10,
        1e-4,
        tmp_path / "checkpoints",
        tmp_path / "validation",
    )

    assert override["label_mappings"] == {"default": [[1, 3], [2, 13]]}
    assert override["finetune_model_path"] == str(bundle / "models" / "model.pt")
    assert override["ckpt_dir"] == str(tmp_path / "checkpoints")
    assert override["patch_size"] == [128, 128, 128]
    assert "drop_label_prob" not in override
    assert "drop_point_prob" not in override


def test_validate_softmax_mapping_accepts_unique_positive_pairs():
    mod.validate_softmax_mapping({"default": [[1, 3], [2, 13]]})


def test_child_process_env_keeps_runtime_values_and_drops_credentials(monkeypatch):
    monkeypatch.setenv("PATH", "/trusted/bin")
    monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "2")
    monkeypatch.setenv("OPENAI_API_KEY", "not-forwarded")
    monkeypatch.setenv("DATABRICKS_TOKEN", "tracking-only")

    env = mod._child_process_env({"NVSEG_FINETUNE_IN_AUTO_VENV": "1"})

    assert env["PATH"] == "/trusted/bin"
    assert env["CUDA_VISIBLE_DEVICES"] == "2"
    assert env["NVSEG_FINETUNE_IN_AUTO_VENV"] == "1"
    assert "OPENAI_API_KEY" not in env
    assert "DATABRICKS_TOKEN" not in env

    tracking_env = mod._child_process_env(extra_keys=mod._MLFLOW_CHILD_ENV_KEYS)
    assert tracking_env["DATABRICKS_TOKEN"] == "tracking-only"
    assert "OPENAI_API_KEY" not in tracking_env


@pytest.mark.parametrize(
    "mapping",
    [
        {"default": []},
        {"default": [[0, 3]]},
        {"default": [[1, 3], [1, 13]]},
        {"default": [[1, 3], [2, 3]]},
    ],
)
def test_validate_softmax_mapping_rejects_invalid_channels(mapping):
    with pytest.raises(mod.typer.BadParameter, match="softmax"):
        mod.validate_softmax_mapping(mapping)


def test_task06_fixture_selects_sanity_preset() -> None:
    assert mod._fixture_preset(Path("/data/Task06")) == "sanity"
    assert mod._fixture_preset(Path("/data/Task06_Lung")) == "sanity"
    assert mod._fixture_preset(Path("/data/spleen_micro")) == "smoke"


def test_sanity_dataset_prefers_explicit_paths(tmp_path):
    fixture = tmp_path / "Task06"
    explicit = tmp_path / "explicit_task06"
    fixture.mkdir()
    explicit.mkdir()

    assert mod._resolve_sanity_dataset(fixture, None) == fixture.resolve()
    assert mod._resolve_sanity_dataset(fixture, explicit) == explicit.resolve()


def test_ensure_smoke_dataset_materializes_missing_niftis(tmp_path):
    dataset = tmp_path / "spleen_micro"
    dataset.mkdir()
    datalist = dataset / "datalist.json"
    datalist.write_text("""
{
  "training": [
    {"image": "imagesTr/spleen_00.nii.gz", "label": "labelsTr/spleen_00.nii.gz", "fold": 0},
    {"image": "imagesTr/spleen_01.nii.gz", "label": "labelsTr/spleen_01.nii.gz", "fold": 1}
  ],
  "testing": []
}
""")

    smoke_dir, smoke_datalist, generated = mod.ensure_smoke_dataset(
        dataset, datalist, tmp_path / "run"
    )

    assert generated is True
    assert smoke_datalist == smoke_dir / "datalist.json"
    assert (smoke_dir / "imagesTr" / "spleen_00.nii.gz").is_file()
    assert (smoke_dir / "labelsTr" / "spleen_01.nii.gz").is_file()


def test_metric_compat_config_stack_skips_when_mean_dice_accepts_num_classes(
    monkeypatch,
):
    monkeypatch.setattr(mod, "_mean_dice_accepts_num_classes", lambda: True)

    assert mod.metric_compat_config_stack() == []


def test_metric_compat_config_stack_writes_only_when_needed(tmp_path, monkeypatch):
    bundle = tmp_path / "bundle"
    monkeypatch.setattr(mod, "BUNDLE_DIR", bundle)
    monkeypatch.setattr(mod, "_mean_dice_accepts_num_classes", lambda: False)

    stack = mod.metric_compat_config_stack()

    assert stack == ["configs/mean_dice_no_num_classes.json"]
    payload = (bundle / "configs" / "mean_dice_no_num_classes.json").read_text()
    assert '"num_classes"' not in payload


def test_sanity_reference_checks_fail_low_recovery_run():
    checks = mod.sanity_reference_checks(
        formal_pretrained=0.6258574724197388,
        formal_finetuned=0.6258574724197388,
        formal_improvement=0.0,
        training_start=0.6326,
        training_best=0.6326,
        training_improvement=0.0,
        best_checkpoint_changed=False,
        overall_rc=0,
    )

    assert checks["passed"] is False
    assert "formal_pretrained_val_dice_ok" in checks["failed_checks"]
    assert "formal_improvement_ok" in checks["failed_checks"]
    assert "training_best_val_dice_ok" in checks["failed_checks"]
    assert "best_checkpoint_changed_ok" in checks["failed_checks"]


def test_sanity_reference_checks_pass_dwf_reference_like_run():
    checks = mod.sanity_reference_checks(
        formal_pretrained=0.67,
        formal_finetuned=0.684,
        formal_improvement=0.014,
        training_start=0.676,
        training_best=0.691,
        training_improvement=0.015,
        best_checkpoint_changed=True,
        overall_rc=0,
    )

    assert checks["passed"] is True
    assert checks["failed_checks"] == []


def test_compare_checkpoint_weights_detects_reserialized_identical_weights(tmp_path):
    torch = pytest.importorskip("torch")
    reference = tmp_path / "reference.pt"
    candidate = tmp_path / "candidate.pt"
    state = {"layer.weight": torch.ones(2, 2)}

    torch.save(state, reference)
    torch.save({"layer.weight": state["layer.weight"].clone()}, candidate)

    comparison = mod.compare_checkpoint_weights(reference, candidate)

    assert comparison["compared"] is True
    assert comparison["weights_identical"] is True
    assert comparison["differing_tensors"] == 0


def test_compare_checkpoint_weights_detects_changed_tensor(tmp_path):
    torch = pytest.importorskip("torch")
    reference = tmp_path / "reference.pt"
    candidate = tmp_path / "candidate.pt"

    torch.save({"layer.weight": torch.ones(2, 2)}, reference)
    torch.save({"layer.weight": torch.zeros(2, 2)}, candidate)

    comparison = mod.compare_checkpoint_weights(reference, candidate)

    assert comparison["compared"] is True
    assert comparison["weights_identical"] is False
    assert comparison["differing_tensors"] == 1
    assert comparison["max_abs_diff"] == 1.0


@pytest.mark.parametrize("state_key", ["state_dict", "model", "network"])
def test_compare_checkpoint_weights_loads_nested_state_safely(tmp_path, monkeypatch, state_key):
    torch = pytest.importorskip("torch")
    reference = tmp_path / "reference.pt"
    candidate = tmp_path / "candidate.pt"
    state = {"layer.weight": torch.ones(2, 2)}
    torch.save(state, reference)
    torch.save({state_key: state, "epoch": 1, "metrics": {"dice": 0.5}}, candidate)
    original_load = torch.load
    calls = []

    def restricted_load(path, *, map_location, weights_only):
        calls.append(path)
        assert map_location == "cpu"
        assert weights_only is True
        return original_load(path, map_location=map_location, weights_only=weights_only)

    monkeypatch.setattr(torch, "load", restricted_load)
    comparison = mod.compare_checkpoint_weights(reference, candidate)

    assert calls == [reference, candidate]
    assert comparison["compared"] is True
    assert comparison["weights_identical"] is True


@pytest.mark.parametrize("unsupported_checkpoint", ["reference", "candidate"])
def test_compare_checkpoint_weights_rejects_objects_without_unsafe_retry(
    tmp_path, monkeypatch, unsupported_checkpoint
):
    torch = pytest.importorskip("torch")
    reference = tmp_path / "reference.pt"
    candidate = tmp_path / "candidate.pt"
    state = {"layer.weight": torch.ones(2, 2)}
    torch.save(state, reference)
    torch.save(state, candidate)
    rejected = reference if unsupported_checkpoint == "reference" else candidate
    # Harmless custom metadata exercises the restricted unpickler's rejection;
    # no executable payload or unrestricted load is needed for this regression.
    torch.save({"state_dict": state, "metadata": SimpleNamespace(epoch=1)}, rejected)
    original_load = torch.load
    calls = []

    def restricted_load(path, *, map_location, weights_only):
        calls.append(path)
        assert map_location == "cpu"
        assert weights_only is True
        return original_load(path, map_location=map_location, weights_only=weights_only)

    monkeypatch.setattr(torch, "load", restricted_load)
    comparison = mod.compare_checkpoint_weights(reference, candidate)

    assert comparison["compared"] is False
    assert comparison["weights_identical"] is None
    assert "error" in comparison
    assert calls == ([reference] if rejected == reference else [reference, candidate])


def test_mlflow_tracking_uses_monai_builtin_without_training_overrides(tmp_path):
    args, metadata = mod.build_mlflow_tracking_args(
        tmp_path,
        tracking_uri=None,
        experiment_name="task06-demo",
        requested_run_name="sanity",
    )

    assert args[:2] == ["--tracking", "mlflow"]
    assert "--tracking_uri" in args
    assert "--experiment_name" in args
    assert "--run_name" in args
    assert args[args.index("--save_execute_config") + 1] == "False"
    assert not any("dataloader" in value or "patch_size" in value for value in args)
    assert metadata["tracking_uri"] == (tmp_path / "mlruns").resolve().as_uri()
    assert metadata["experiment_name"] == "task06-demo"
    assert metadata["run_name"] == "sanity"
    assert args[args.index("--run_name") + 1] == "sanity"


def test_mlflow_tracking_preserves_explicit_remote_uri(tmp_path):
    args, metadata = mod.build_mlflow_tracking_args(
        tmp_path,
        tracking_uri="databricks",
        experiment_name="/Shared/nvseg",
        requested_run_name=None,
    )

    assert args[args.index("--tracking_uri") + 1] == "databricks"
    assert metadata["tracking_uri"] == "databricks"
    assert "--run_name" not in args
    assert "run_name" not in metadata

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 a command-line tool, Needs Python

Security

What our scanner noticed· Medium risk

  • Large base64-encoded blobs
  • Instructions that use the network
  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

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 MedTech
Publisher
NVIDIA (Official)
Source
NVIDIA/skills · skills/nv-segment-ct-finetune · 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 MedTech or NVIDIA.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download nv-segment-ct-finetune.zip