AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The cdf-anomalygen skill manages the complete lifecycle of the AnomalyGen pipeline, which fine-tunes a Cosmos base model to generate realistic synthetic defect images. It handles mask placement, model training, synthetic data generation, evaluation, and quality refinement. The skill supports both agentic (AMG) and classic (AMP) mask placement backends, ensuring that defect datasets are processed according to the required contract layout.
What it does
- Automates the full AnomalyGen pipeline from mask placement to pseudo-labeling.
- Supports flexible execution modes: full pipeline, fine-tuning only, or generation only.
- Provides two mask placement backends: agentic (AMG) for VLM-based proposals and classic (AMP) for rule-based allocation.
- Manages model registry configuration for VLM-based mask generation.
- Handles recipe management and training configuration for fine-tuning runs.
- Enforces strict verification gates at every stage to ensure data integrity and pipeline success.
When to use it
- When you need to generate synthetic defect images for training anomaly detectors.
- When fine-tuning a Cosmos-based model on a specific defect dataset.
- When performing mask placement on clean images using either agentic or classic methods.
- When evaluating or refining the quality of generated synthetic defect datasets.
Advantages
- Ensures consistent pipeline execution through standardized verification gates.
- Supports both modern agentic and legacy rule-based mask placement workflows.
- Provides clear separation between experiment labels and dataset locations.
- Facilitates reproducible training runs via structured recipe management.
Usage examples
- “Run the AnomalyGen pipeline on the phone_screen dataset.”
- The agent initiates the full pipeline (Steps 1-7) using the specified dataset, managing mask placement, fine-tuning, and generation.
- “Fine-tune an anomaly-generation model for PCB defects.”
- The agent executes the fine-tuning mode (Steps 1-3) to train the model on the provided PCB defect dataset.
- “Generate 25 synthetic PCB anomaly images.”
- The agent runs the generation-only mode, using an existing checkpoint to produce 25 synthetic images based on the specified recipe.
- “Pseudo-label the generated defects.”
- The agent performs the pseudo-labeling stage of the pipeline on the existing generated dataset.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/nvidia/cdf-anomalygen/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip cdf-anomalygen.zip -d .agents/skills
Documentation by NVIDIA
Skill: cdf-anomalygen
Purpose
AnomalyGen fine-tunes the frozen Cosmos base into an anomaly-generation model: given a clean image + defect mask, it paints a realistic defect into that region (synthetic data for detectors). Pick a mode, collect its params, run the steps — all commands from the repo root.
When to Use This Skill
Use for the CDF AnomalyGen pipeline over a defect dataset — any single stage (mask placement, fine-tuning, generation, evaluation, refinement, or pseudo-labeling) or the full run.
"Run the AnomalyGen pipeline on phone_screen"
"Fine-tune an anomaly-generation model for PCB defects"
"Generate 25 synthetic PCB anomaly images"
"Place defect masks", "evaluate quality", or "pseudo-label the generated defects"
Do not use this skill for generic image generation, or for synthetic data that isn't defect images
built from the {texture}/{defect} clean-image + defect-mask contract.
Prerequisites
A CUDA GPU and the project's built env (native
.venvor the Docker image).Hugging Face auth — run
hf auth login— plus network access; the downloader fetches the model checkpoints.A
dataset_dirin the Step 1 contract layout (clean images +{defect}masks; plusdefect_spec.jsonlonly whenmask_backend=amp— the defaultamgnever reads it, and nothing after Step 2 does either).
Preflight the env + checkpoints (GPU, Python, CUDA/torch, deps, HF auth, model checkpoints):
scripts/preflight_env_ckpt.shRead which group failed before reacting — the summary separates them, and only one is fixed by downloading:
missing checkpoints →
scripts/download_checkpoints.sh(runhf auth loginfirst).env / Python / deps → rebuild the environment (
bash scripts/env_setup.sh, then activate it).
Third group — model access for mask placement (checked HERE, never mid-flow). The default
mask_backend=amg needs a vision model, and a run that discovers this at Step 2 has already spent
the user's time. A VLM API is a base, a key and a model name — and all three live in ONE place:
~/.anomalygen/endpoints.yaml, the model registry (nothing is built into the code; the template
is scripts/agentic_mask_generation_serving/endpoints.example.yaml). It sits under $HOME, not in
the repo, so it works from any directory, serves every checkout, and is writable inside the
read-only product containers. The moment inputs are locked with mask_backend=amg (the default), probe:
[ -f ~/.anomalygen/endpoints.yaml ] && echo "amg model: ~/.anomalygen/endpoints.yaml present" \
|| echo "amg model: no ~/.anomalygen/endpoints.yaml — NO model can resolve; copy scripts/agentic_mask_generation_serving/endpoints.example.yaml there"The model resolves in the registry → use it. That is the whole check: --gen-model and
--judge-model are REQUIRED flags (there is no default model), and every name they carry must have
a registry entry (model + base_url + key) — the entry point refuses to start when one does
not; there is no environment fallback. A user's own OpenAI-compatible endpoint is the same case:
one endpoints.yaml entry with their documented base URL, their key, and their model name in
--gen-model/--judge-model.
Anything less → stop and ask the user now, before Step 1 — unless the user's request already
names the model (e.g. "use local Gemma"): that IS the pick, act on it without re-asking. Three
options — whichever it is, set AMG_MODEL to the chosen name; the Step 2 command passes it to
both required flags:
(a) supply an API — one
endpoints.yamlentry: a model name, a base and a key (e.g.AMG_MODEL=azure/openai/gpt-6-astra).(b) let the skill start the model service — in a container, as a background process inside it; on a dev host, from
scripts/agentic_mask_generation_serving/. Needs a GPU; the weights are a default checkpoint —scripts/download_checkpoints.sh gemmafetches them and preflight verifies them (the group argument fetches only this model; bare, the script fetches all eleven).mkdir -p ~/.anomalygen && cp scripts/agentic_mask_generation_serving/endpoints.example.yaml ~/.anomalygen/endpoints.yamlwires it: the template's gemma entry already routesAMG_MODEL=nvidia/google/gemma-4-31b-ittohttp://localhost:8902/v1, and the run carries--beam-multi 4 --beam-pairwise --exemplar-captions --exemplar-visual-prompt --coverage-note— the local model always pairs with the in-loop beam and the three information channels. Bootstrap, lifecycle and the why:agentic-mask-generation.md.(c) set
mask_backend=ampfor this run.
Never fall back silently. Whichever they pick, the run's own capability probe confirms the model can take images and answer in JSON before Step 2 spends anything.
Inputs
Collect the run's inputs from the user before starting. If the mode or any input it requires is
missing, ask the user first — never guess a required input or silently fall back to its default.
Lock the mode first — it fixes which steps run and which inputs are required (ask for any not
provided):
full— train + generate, Steps 1 → 7. Required:name,dataset_dir,num_sdg.finetune_only— train only, Steps 1 → 3. Required:name,dataset_dir,num_sdg. Herenum_sdgsizes only the validation set.generation_only(a.k.a.inference_only) — generate from an existing checkpoint, Steps 2, 4 → 7. Required:name,checkpoint,recipe,dataset_dir,num_sdg. The checkpoint's geometry stamp must equalrecipe's. A legacy checkpoint (trained before the stamp existed) has none: pass its run-dir recipe copy and it is stamped from that recipe at load (generation only; it cannot be resumed).
name is required in every mode — an experiment label that names the recipe, run dir, logs and
the generated mask-set dirs. It is never a dataset location: where the data lives is dataset_dir,
always passed separately and never derived from name.
Optional in every mode — all have working defaults, so ask only if the user raises them:
num_gpus (1), num_search_run (3), score (nn), task (texture_ft),
base_seed (1), mask_backend (amg), legacy_fixed_size (off), per_clean_pairing (off).
Anything not listed as Required above is optional.
legacy_fixed_size switches Step 3 to the legacy fixed-input-size training mechanism. It is off by
default: every fine-tune uses the current variable-input-size mechanism unless the user explicitly
asks for the legacy one ("FT using the legacy fixed-size mechanism", "old fixed input size setting",
"previous release FT mode", etc.). Never infer it from the dataset, the image sizes, or KPI numbers,
and never ask about it when the user has not raised it. When set, the run takes
job_name: anomalygen_texture_ft_legacy — in the recipe and as --job_name to set_pipeline_vars.sh
— so it never shares a run dir with a default run of the same name, and Step 3 appends the legacy
block to the recipe.
mask_backend picks how Step 2 places masks: amg (default) — the agentic engine proposes,
refines and judge-accepts every mask, all defect types included; amp — the
allocate → pair → place pipeline; no VLM beyond text ROIs. AMG
needs model access (preflight checks before spending); if the user cannot supply it and does not choose,
stop and ask — never fall back silently.
Parameter tables (each mapped to its CLI arg) and the step map: inputs.md.
Confirm the inputs, set the shared variables below, then run straight through — in full,
checkpoint is derived after Step 3, so don't pass it.
Instructions
Run the steps your mode selects (see Inputs), in order — each step's header notes when to skip it.
A step is done only when its output exists on disk — not when its command is launched. Every step ends with a named gate (dataset, amp, train, …); run it before moving on. Never treat starting a long job as a stopping point, nor proceed on a partial artifact. Gate commands and the fix for each failure:
references/verification.md.
Set the shared variables every step below refers to. Re-source this in each new shell — variables
do not survive one — and read the values it echoes back (${DATASET_DIR}, ${VAL_DIR}, ${GEN_DIR},
${OUT}, ${RUN_DIR}, ${LOG_DIR}, …):
source scripts/skill_utility/set_pipeline_vars.sh --name <name> --dataset_dir <dataset_dir> \
--num_sdg <num_sdg> [--num_gpus 1] [--num_search_run 3] [--task texture_ft] \
[--job_name anomalygen_texture_ft_legacy] # legacy_fixed_size only
export SCORE="<score>" # repeat the confirmed choice in every shell; use nn when none was requested--dataset_dir is required and is never derived from --name. The name is an experiment label; the
dataset is a separate input that comes from the user. They differ in the normal case — a label of
uc1pega-opus5-e2e over a dataset at datasets/uc1pega — so a name-derived path would be wrong far
more often than right, and wrong silently: the recipe's dataset_path is set separately, so the
recipe can point at the right dataset while ${DATASET_DIR} hands Steps 5 and 6 a --real_root that
scores against nothing. The script does not check that the path exists; gate dataset does, one
step later.
Every step's log goes under ${LOG_DIR} (results/), never the repo root. Use
${LOG_DIR}/<step>_${NAME}.log. The same goes for PID files — the launcher writes its own under results/;
do not add one.
The steps below pipe through tee to keep the log and the console. Run set -o pipefail first —
without it the pipeline's exit status is tee's, so a step that died reports success:
set -o pipefailset_pipeline_vars.sh rejects a name/task outside [A-Za-z0-9][A-Za-z0-9._-]* and any non-integer count rather
than sanitizing them — both reach command lines below. Apply the same care to user-supplied paths
(dataset_dir, recipe, checkpoint): quote every expansion. texture_ft is the only task_type
today; a new task family adds one ${TASK}→${SCRIPTS} line to that script plus a matching
task_type: in the recipe.
Step 3 sets CKPT and repoints RECIPE at the run-dir copy for Steps 4–7. generation_only skips
Step 3, so set both yourself before Step 4 (use the user's checkpoint verbatim if given; CKPT and
RECIPE must come from one run or class ids drift):
RUN_DIR=<existing run dir> # only if it is not the ${RUN_DIR} already set above
CKPT="${RUN_DIR}/checkpoints/model/$(cat "${RUN_DIR}/checkpoints/best_checkpoint.txt")"
RECIPE="${RUN_DIR}/exp_${TASK}_${NAME}.yaml" # run-dir copy — MUST match the checkpointStep 1 — dataset (contract layout)
All modes need dataset_dir on disk in this layout — training and eval read the
anomaly_image/+mask/ pairs; both Step 2 backends read clean_image/ (AMG also samples those
same pairs as exemplars); defect_spec.jsonl has exactly one consumer, the AMP backend:
{dataset_dir}/{texture}/anomaly_image/{defect}/<name>.png # defect image
{dataset_dir}/{texture}/mask/{defect}/<name>_mask.png # binary mask (>=128 = defect); same stem + _mask
{dataset_dir}/{texture}/clean_image/<name>.png # clean images (Step 2 inputs — both backends)
{dataset_dir}/{texture}/cad_mask/<clean_stem>.png # cad/cad_tht only: one per clean image, no {defect} level
{dataset_dir}/semantic_segmentation_labels.json # cad/cad_tht only: label map for those CAD masks
{dataset_dir}/defect_spec.jsonl # amp only: one row per {texture}+{defect}; sets spatial_dependency (free/text/cad/cad_tht). amg never reads it — absent is normalReady-made subjects:
python -m anomalygen.scripts.datasets.prepare_phone_screen_defect --output_dir "${DATASET_DIR}" --zip <roboflow.zip>
# --zip is required (the images are a manual Roboflow download); --masks_only asks for a dataset with no images.
# also: prepare_pcb_defect, prepare_magnetic_tile_defect (see anomalygen/scripts/datasets/README.md)For custom data, emit the same layout and list every [texture, defect] pair in the recipe's
anomaly_types. Confirm the layout before proceeding — gate dataset.
Step 2 — mask placement
Two backends produce the same artifact (placed masks + testcase.jsonl); everything downstream is
backend-blind. Default is AMG; set mask_backend=amp for the classic pipeline.
Model access was verified at preflight (third group). If preflight was skipped, run that probe now — it must pass before anything below spends money.
One notice: if defect_spec.jsonl is present and declares spatial_dependency, tell the user —
AMG is the default and does not read that field (it decides placement by looking at the board);
to honour free/text/cad routing, run with mask_backend=amp. Do not change the default over
it. A dataset with no defect_spec.jsonl at all is normal under amg — nothing in the pipeline
reads it then, and the grep below is simply silent; under amp its absence is a dataset gate
failure.
grep -l spatial_dependency "${DEFECT_SPEC}" >/dev/null 2>&1 && echo "NOTE: spatial_dependency present but mask_backend=amg ignores it; use mask_backend=amp to honour it"AMG (default)
Reference (model resolution, the model-service bootstrap, the capability probe, cost, resume, the
run's own deviation ledger): agentic-mask-generation.md.
One run per output set, each sized by --num-sdg — the same request AMP answers, split across the
types by the same rule (base each, remainder to the first types). Both sets use ${NUM_SDG}, the
same convention as AMP, where --num_sdg sizes the validation and the generation set alike. The
deliverable is exactly num_sdg rows, so gate amg checks the number that was asked for:
AMG_MODE=validation; AMG_DIR="${VAL_DIR}"; ALLOC=validation # names the log, the out-dir, the split
# then again: AMG_MODE=generation AMG_DIR="${GEN_DIR}" ALLOC=inference
python -m anomalygen.scripts.agentic_mask_generation.generate \
--dataset-path "${DATASET_DIR}" --anomaly-types <texture>+<defect> [...] \
--gen-model "${AMG_MODEL}" --judge-model "${AMG_MODEL}" \
--model-registry "${AMG_REGISTRY:-$HOME/.anomalygen/endpoints.yaml}" \
--num-sdg "${NUM_SDG}" --mode "${ALLOC}" \
--visual-prompt auto --defect-library auto \
--out-dir "${AMG_DIR}" 2>&1 | tee "${LOG_DIR}/amg_${AMG_MODE}_${NAME}.log"--model-registry is explicit, not left to the default, because Step 2 may run inside a container
whose $HOME is not the host's: the flag makes "which registry answered" a fact of the recorded
command. Set AMG_REGISTRY to wherever the registry actually is (e.g. the path it was mounted at).
--mode is why the two sets are not interchangeable. The generation set is inference: an even
split, and a type may legitimately get 0 when num_sdg is below the type count. The validation set is
validation: split in proportion to each type's real mask count, so it mirrors what the model trains
on — and a share that would round to 0 is refused, because validation scores per type and a type
with no samples drops silently out of the KPI that picks best_checkpoint.txt. Both are AMP's rules,
shared through one allocation module rather than reimplemented.
--num-sdg is the only way to size a run, as it is for AMP. It splits the total across the
types rather than sizing each alike, so 25 over 3 types is 9/8/8 and lands on 25 — where a per-type
count could only reach 27, paying for two masks before anything discards them. For exact per-type
targets instead, --per-defect-counts '{"<texture>+<defect>": N}'; omitted types get 0.
AMG_MODE is in the log name because there are two runs. Both would otherwise tee to one file
and the second would overwrite the first, silently discarding the validation run's deviation ledger —
the same reason AMP's log carries ${AMP_MODE}.
The validation set's size is a training-time cost, not just a one-off API charge. Every validation
pass re-renders the whole testcase_jsonl set and runs one SAM2 forward per sample (~0.4 s each — see
fine-tuning.md), repeated max_iter / validation_iter times. To spend
less, lower num_sdg; do not raise validation_iter, which also coarsens the resolution
best_checkpoint.txt is chosen at.
AMG places every type it is given. A stopped or interrupted run
continues from where it was when re-run with the same command. The run records every deviation as
degraded[KIND] in its log — read the summary before trusting the output. Verify the deliverable
directly against gate amg (verification.md): testcase.jsonl rows
= ${NUM_SDG}. The fill loop delivers in beam-sized batches and then trims to the target, so
trimmed from N with N well above a type's share is normal, not a defect — the surplus is kept under
{texture}/_overflow/{defect}/ + testcase_overflow.jsonl and is deliberately not counted in
testcase.jsonl, so counting mask files on disk will exceed the row count with nothing wrong. When
the user asks for different backends on different types, each backend places its own subset and this
count is over the union of the runs' rows (the AMP gate's roi_* checks and remedies are for AMP
out-dirs only).
When the model is served here, the service spans Step 2 and nothing else. Both mask sets are Step
2 products, so serve once and run both against it — then stop it. Serving holds a large share of the
GPU, and Step 3 (full) and Step 4 (generation_only) need that GPU back:
scripts/agentic_mask_generation_serving/serverctl.sh health # idempotent: probe first, start only if down
case $? in # 0 up · 1 not running · 2 alive but loading
0) : ;; # ready — run the two generate runs below
1) echo "DOWN — start it (references/agentic-mask-generation.md), then wait for it" ;;
2) echo "LOADING — call: scripts/agentic_mask_generation_serving/serverctl.sh wait (repeat while it exits 2)" ;;
esac
# ... the two generate runs above: AMG_DIR=${VAL_DIR}, then AMG_DIR=${GEN_DIR} ...
scripts/agentic_mask_generation_serving/serverctl.sh stop # part of this step's gate
scripts/agentic_mask_generation_serving/serverctl.sh health # ONLY exit 1 means the GPU is free
[ $? = 1 ] || echo "STILL HOLDING THE GPU — Step 3 will contend for it"Test for exit 1, not for failure. A server that is up (0) and one still loading (2) both hold their
--gpu-memory-utilization 0.9 reservation, so health && echo ... would clear the gate on a process
that never finished starting and is still sitting on the card.
Waiting for the service is an action, exactly as waiting for a fine-tune is. serverctl.sh wait
reports state as its exit code — 0 up, 1 no such process, 2 still loading, call again — and
each call is a ~9.5-minute slice, not the whole wait. A single-GPU cold start measured ~14 minutes
to first answer /v1/models, so expect two or three calls. Do not read a DOWN/LOADING line during
that window as a failed start: exit 2 means the process is alive and has not finished loading, and
restarting on that reading puts a second vLLM on the same GPU at --gpu-memory-utilization 0.9.
Step 2 is not done until that last line prints nothing. The bootstrap that starts it — in a
container or on a dev host — is in agentic-mask-generation.md.
Classic AMP (mask_backend=amp)
Reference (allocation modes, KPI floor, --per_defect_counts, ROI caching, free/text/cad/cad_tht
routing, pairing coverage): mask-placement.md.
AMP builds a testcase.jsonl (clean image + placed mask + gen params) by running allocate → pair →
place. Build the validation set (fine-tune) and/or the generation set (SDG), each with a
distinct --seed — full → both, finetune_only → validation, generation_only → generation:
AMP_MODE=validation; AMP_DIR="${VAL_DIR}"; SEED=42 # or: AMP_MODE=inference AMP_DIR="${GEN_DIR}" SEED=43
python -m anomalygen.scripts.auto_mask_placement.roi_place_pipeline \
--num_sdg "${NUM_SDG}" --mode "${AMP_MODE}" --defect_desc "${DEFECT_SPEC}" \
--dataset_dir "${DATASET_DIR}" --output_dir "${AMP_DIR}" --seed "${SEED}" \
2>&1 | tee "${LOG_DIR}/amp_${AMP_MODE}_${NAME}.log"
# flags for roi_place go after a bare --, e.g.: -- --refresh_roiIt derives every intermediate path from --output_dir, threads roi_pair's n_seeds into
roi_place, and stops with a named stage if one produces nothing. Output is placed masks +
${AMP_DIR}/testcase.jsonl. The generation set must have exactly as many rows as the
allocation asked for — num_sdg, or the --per_defect_counts sum when you passed one — verify before
proceeding: gate amp.
Only when the user set per_clean_pairing=on: pass --per_clean_pairing on, and with it a
single-type --per_defect_counts '{"<texture>+<defect>": '"${NUM_SDG}"'}' with num_sdg at the
clean-image count — on is one record per clean, so it takes exactly one defect type. Absent, the
default off pairing applies: it maximises (submask, clean) diversity and so visits only some
clean images (mask-placement.md). Whether every clean must receive
an output is a property of the batch that only the user knows; do not infer it.
Step 3 — fine-tuning (skip when generation_only)
Reference (recipe knobs, launch mechanics, early stopping, checkpoint selection):
fine-tuning.md.
Before training: copy the template recipe and edit its dataset block so testcase_jsonl points at
the validation set from Step 2 (AMG or AMP) (other keys ship at tested defaults):
TEMPLATE=ag_config/exp_texture_ft_phone_screen.yaml # the worked example doubles as the template
[ -f "${USER_RECIPE:-}" ] && TEMPLATE="${USER_RECIPE}" # a recipe the user passed replaces it
[ "${RECIPE}" = "${TEMPLATE}" ] || cp "${TEMPLATE}" "${RECIPE}" # same file (name=phone_screen or a passed recipe): edit it in placeIf the user passed a recipe, set USER_RECIPE to it first: an existing file replaces the template,
so a recipe already at ${RECIPE} is edited in place, never overwritten.
# edit the dataset block in ${RECIPE}
dataset_name: ${NAME}
anomaly_types: [[<texture>, <defect>], ...] # every [texture, defect] pair on disk
dataset_path: ${DATASET_DIR}
testcase_jsonl: ${VAL_DIR}/testcase.jsonl # ← Step 2 validation setIf you change max_iter, set cycle_lengths to the same value. cycle_lengths sets how long the
learning-rate schedule lasts, and training crashes if max_iter is larger.
Only when legacy_fixed_size is set: change the recipe's job_name line to
anomalygen_texture_ft_legacy (the value you passed as --job_name), and append this block to
${RECIPE}, keys and values exactly as shown — it is the legacy mechanism as one unit, never a subset:
# legacy_fixed_size only — append to ${RECIPE}
input_size_ladder: [512]
crop_mode: ratio
crop_ratio_range: [1.5, 8.0]
background_dropout_prob: 0.5
ring_jitter_prob: 0.5
inst_aug_prob: 0.5A fine-tune runs hours at the 15000 default, so it is launched detached (own session, PID file, log).
Check ${RECIPE} is the one you meant before launching — it then runs unattended with your
credentials:
scripts/skill_utility/train_control.sh start --name "${NAME}" --recipe "${RECIPE}" \
--num_gpus "${NUM_GPUS}" --scripts "${SCRIPTS}" --task "${TASK}"This launcher is the only sanctioned way to start a fine-tune. Never hand-roll a torchrun for
training. It is what writes results/train_${NAME}.log and results/train_${NAME}.pid, records the
run dir, and makes wait/stop work; a direct torchrun bypasses all of it, and a literal
--nproc_per_node=<n> typed at the prompt silently discards ${NUM_GPUS} and the user's GPU budget
with it. Pass --num_gpus ${NUM_GPUS} — never a literal.
Writes ${RUN_DIR} (recipe copied in). train_control.sh stop --name ${NAME} ends a run;
start again resumes from latest_checkpoint.txt.
Waiting is an action. Call wait in the foreground until it stops returning 2 — arming a
background watcher and ending your turn leaves nobody to run Steps 4–7:
scripts/skill_utility/train_control.sh wait --name "${NAME}" # 0 = finished · 1 = failed · 2 = still running, call againEach call is a ~9.5-minute slice, not the whole wait. The 15000 default runs for hours
(dataset-dependent; the measured times are in docs/tutorial/texture/7-agentic-end-to-end-flow.md) —
expect dozens of calls.
Only exit 0 clears gate train — not a slice expiring, and not a checkpoint appearing
(the trainer writes an intermediate iter_<N>.pt every save_iter). On 1, read the log
and fix the run; do not proceed.
finetune_only stops here. For full, take the best iteration from the pointer the trainer
writes at train end (peak nn_score on the Average column) — runs routinely peak mid-training:
CKPT="${RUN_DIR}/checkpoints/model/$(cat "${RUN_DIR}/checkpoints/best_checkpoint.txt")" # iter_<best>.pt
RECIPE="${RUN_DIR}/exp_${TASK}_${NAME}.yaml" # run-dir copy — MUST match the checkpointNever substitute latest_checkpoint.txt — it names the last iteration. ${RUN_DIR} also resolves
correctly (best pointer first, then latest, then highest iter_*.pt), but pass the explicit path so
the record names the exact checkpoint. A missing pointer is a train gate failure — fix the run,
don't let it fall back to the latest.
Step 4 — generation / SDG (skip when finetune_only)
Reads the generation AMP testcase.jsonl (Step 2, GEN_DIR). --recipe must be the recipe the
checkpoint was trained with (use the run-dir copy) so anomaly_type→class-id lines up.
torchrun --nproc_per_node="${NUM_GPUS}" "${SCRIPTS}/generate.py" \
--checkpoint "${CKPT}" \
--recipe "${RECIPE}" \
--input_data_path "${GEN_DIR}/testcase.jsonl" \
--output_dir "${OUT}" \
2>&1 | tee "${LOG_DIR}/generation_${NAME}.log"--nproc_per_node=${NUM_GPUS} — expand the variable, never a literal count.
--base_seed (default 1) seeds the per-testcase noise. Don't change it mid-pipeline — Step 6
compares KPIs across re-generated rounds, which a different seed makes incomparable.
Output tree: reconstructed_image/ (the synthetic defects), original_image/, original_mask/, and
${TASK}_generation_result.csv. generate.py emits one image per testcase row (no quality drop),
except for samples the content-safety guardrail blocks — those write no image and land in
guardrail_blocked.csv, which is expected, not a failure. Verify images + blocked = rows, and the
canonical CSV header, before Step 5 (gate generate); never hand-edit the CSV — a non-canonical
CSV crashes Step 6.
Step 5 — evaluation (skip when finetune_only)
Scores the batch against the real dataset with the same correspondence KPI as training validation.
python "${SCRIPTS}/evaluate.py" \
--gen_root "${OUT}" --real_root "${DATASET_DIR}" \
--recipe "${RECIPE}" \
--output_file "${OUT}/${NAME}_kpi.json" \
2>&1 | tee "${LOG_DIR}/evaluation_${NAME}.log"Writes per-type + Average blocks (nn_score, mnn_score, fid, per_sample[]) — verify it exists
(gate eval). nn_score is the primary KPI (higher is better); mnn_score/fid are secondary.
It also scores the anomaly-quality axes and aq_nn, the same metrics validation records
(fine-tuning.md). --anomaly_types Phone+oil ... overrides
recipe-derived types. The per-sample rows carry NN, completeness, precision, and boundary IoU, which
Step 6 needs when --score aq_rank is selected.
Step 6 — quality refinement (skip when finetune_only)
Reference (draw strategy, ranges/seeding, select):
quality-refinement.md.
quality_refine.py run does the whole loop: (draw → generate → evaluate) ×${NUM_SEARCH_RUN}, then select and a final
score. Each round's (guidance, crop_ratio) is drawn per sample — round 1 uniform, rounds 2+
per-sample Bayesian optimization (see the reference):
python "${SCRIPTS}/quality_refine.py" run \
--base_testcase "${GEN_DIR}/testcase.jsonl" \
--original "${OUT}" --original_kpi "${OUT}/${NAME}_kpi.json" \
--rounds_dir "${OUT}/rounds" --output "${OUT}/searched" \
--final_kpi "${OUT}/searched/${NAME}_kpi.json" \
--checkpoint "${CKPT}" --recipe "${RECIPE}" --real_root "${DATASET_DIR}" \
--score "${SCORE}" \
--num_search_run "${NUM_SEARCH_RUN}" --num_gpus "${NUM_GPUS}" \
2>&1 | tee "${LOG_DIR}/refine_${NAME}.log"It refuses to continue past a round that did not finish — completed/blocked rows, a generation CSV
that names exactly the reconstructed images, and its kpi.json. Both BO and select key off those
artifacts, so an unfinished round would otherwise be dropped silently and its GPU time wasted.
select always runs: num_search_run=0 reduces it to cloning ${OUT} into searched/, so
searched/ always exists for Step 7. Verify the round dirs before Step 7: gate refine.
nn remains the default score. --score aq_rank is an optional cumulative composite: before each
draw it pools Original with every earlier round, groups candidates by anomaly type, and recomputes
AQ-Rank from NN, completeness, precision, and boundary IoU. Final selection recomputes it once more
over Original plus only the rounds requested by the current run. It therefore does not reuse the
bucket-local aq_rank_score written by evaluate.py whenever candidates compete; the zero-round
clone may copy it into audit output but makes no ranking decision. A candidate missing any of the
four finite raw inputs is logged and excluded from the rank pool, so it cannot earn a favorable rank
on a negatively oriented axis; the rest of the bucket remains usable. If one sample has no eligible
render across any bucket, selection logs the fallback and keeps its Original render without calling
it an AQ-Rank win. A bucket with no eligible candidate still fails explicitly.
AQ-Rank fits each geometry-axis direction across the pooled samples but applies it when comparing round variants within one sample; those relationships can disagree. Every re-ranking logs each axis's AUROC and sign for audit. Treat this as an opt-in heuristic, not a within-sample improvement guarantee; changing to a within-sample orientation requires a separate experiment and human validation.
With --score nn, the Average.nn_score delta between ${OUT}/searched/${NAME}_kpi.json and the
Step 5 ${OUT}/${NAME}_kpi.json measures the refinement gain. AQ-Rank is pool-relative, so do not
compare its mean across passes. For --score aq_rank, inspect winner-source distribution and paired
raw-metric changes, then use human or downstream evaluation for the final quality claim. A
seven-dataset, one-reviewer blind pilot found that NN and AQ-Rank both continued selecting later-round
candidates and detected no statistically significant perceptual difference between their final
outputs; this supports AQ-Rank as an option, not a claim that it is superior or equivalent to NN.
Step 7 — pseudo-labeling (skip when finetune_only)
Turns the refined ${OUT}/searched tree into a labeled dataset: COCO instance annotations, a
classification layout, visualizations, and (unless --no_caption) VLM captions.
python "${SCRIPTS}/pseudo_label.py" \
--gen_root "${OUT}/searched" \
--output_dir "${OUT}/searched/pseudo_labels" \
2>&1 | tee "${LOG_DIR}/pseudo_label_${NAME}.log"
# add --no_caption to skip the VLM captioning (fast, no GPU model load)--gen_root must contain reconstructed_image/, original_image/, original_mask/, and
${TASK}_generation_result.csv (Step 6 always produces this at ${OUT}/searched). Verify the labels
landed — gate label.
Verification
The artifact each gate requires. Runnable commands + the fix for each failure:
verification.md.
| # | Gate | Step | Requires |
|---|---|---|---|
| 1 | dataset | 1 | per-pair clean_image/, anomaly_image/{defect}/, mask/{defect}/; + defect_spec.jsonl only when mask_backend=amp |
| 2a | amg | 2 | each set's testcase.jsonl; rows = num_sdg; no type_failed / model_smoke_failed |
| 2b | amp | 2 | ${AMP_DIR}/testcase.jsonl; generation row count = the allocation |
| 3 | train | 3 | wait exit 0 and checkpoints/best_checkpoint.txt |
| 4 | generate | 4 | canonical CSV header and images + guardrail_blocked.csv rows = testcase rows |
| 5 | eval | 5 | ${OUT}/${NAME}_kpi.json with per-type + Average |
| 6 | refine | 6 | ${NUM_SEARCH_RUN} round dirs, each complete (images and kpi.json) |
| 7 | label | 7 | pseudo_labels/coco_annotations.json + classification/classes.txt |
Gate 6 counts complete rounds — existing dirs aren't enough. An all-original searched/ source
column is a valid outcome (no round beat Step 4), not a failure; missing round dirs are.
Examples
Natural invocations — the key=value params map to the Inputs table (recipe knobs like max_iter
edit the recipe; see references/inputs.md). pcb is cad-routed,
magnetic_tile is free-routed, phone_screen is text-routed, and a through-hole solder-joint set
is cad_tht-routed, so their Step 2 AMP differs
(see references/mask-placement.md).
Full pipeline (Steps 1→7) on the text-grounded phone-screen set:
Use AnomalyGen skill with
name=phone_screen
mode=full
dataset_dir=datasets/phone_screen
num_sdg=25
max_iter=15000
validation_iter=1000
save_iter=1000To generate more from an existing checkpoint instead, switch to mode=generation_only and add
recipe=${RUN_DIR}/exp_${TASK}_<name>.yaml + checkpoint=${RUN_DIR}/checkpoints/model/iter_<best>.pt
(from best_checkpoint.txt), dropping the training knobs (max_iter/validation_iter/save_iter).
Only a prompt that explicitly asks for it — "FT using the legacy fixed-size mechanism" — adds
legacy_fixed_size=true: pass --job_name anomalygen_texture_ft_legacy, set the same job_name in the
recipe and append the legacy block (Step 3). The run lands under
results/anomalygen/${NAME}/anomalygen_texture_ft_legacy; SDG outputs under results/generation_${NAME}
are still shared with a default run of the same name. Every other prompt trains with the default
variable-input-size mechanism.
Troubleshooting
Each fails quietly — plausible output, wrong result. Detail in
fine-tuning.md and mask-placement.md.
| Symptom | Cause / fix |
|---|---|
| Class ids misaligned in the labels | A different anomaly_types set — the checkpoint stamp refuses it; use the run-dir recipe copy (order is sorted at parse, so it never matters) |
| Validation scores nothing | testcase_jsonl not wired to the Step 2 validation set |
A fresh max_iter run exits immediately | Resumed an old iter_*.pt — clear it or set a new job_name |
A legacy run dies after model load with a geometry-stamp refusal (crop_mode / input_size_ladder differ) | It resumed a default run's checkpoint from the same run dir — the recipe's job_name and --job_name were not both anomalygen_texture_ft_legacy; fix both and start again |
| Fewer rows than the allocation | A defect yielded zero placements — check roi_place logs |
| A worse checkpoint ships, numbers look fine | Read column 1 of ${RUN_DIR}/valid/<iter>/valid_kpi.csv (one per validated iteration, not one file), not Average — use best_checkpoint.txt |
Files
- ATTRIBUTION.mdAdded by AutoGineer900 B
- BENCHMARK.md7.6 KiB
- LICENSELicence file added by AutoGineer11.2 KiB
- SKILL.md38.7 KiB
- evals/evals.json5.8 KiB
- references/agentic-mask-generation.md21.3 KiB
- references/fine-tuning.md20.0 KiB
- references/inputs.md9.6 KiB
- references/mask-placement.md8.9 KiB
- references/quality-refinement.md12.6 KiB
- references/verification.md17.3 KiB
- skill-card.md4.9 KiB
- skill.oms.sig5.9 KiB
references/agentic-mask-generation.md
Agentic mask generation reference — Step 2, mask_backend=amg (the default)
Progressive-disclosure detail for Step 2 — mask placement in the parent SKILL.md. Read this for
model resolution, the model-service bootstrap and its lifecycle, what a run costs, resume, and how a
run reports its own deviations. If this file conflicts with SKILL.md, SKILL.md wins.
What it is, in one paragraph
AMG is an agentic alternative to allocate → pair → place: a VLM proposes
candidate masks on the clean image, refines them over several rounds, and an independent
judge scores each on six dimensions; only masks that clear the acceptance bar are delivered. It
reads the same dataset layout as AMP (clean images + per-defect example images and masks), needs no
defect_spec routing, and writes the same deliverable: placed masks + testcase.jsonl. Downstream
steps cannot tell which backend produced their input.
Model resolution — two cases
A VLM API is a base, a key and a model name, wherever those come from. AMG makes no distinction
between one vendor's endpoint and another's, or between a hosted service and one running on this
host: they are the same three settings, and all three live in ONE place — ~/.anomalygen/endpoints.yaml,
the model registry (--model-registry <path> to use another file; the $HOME one is picked up
automatically from any directory, and stays writable inside the read-only product containers).
Nothing is built into the code: the template scripts/agentic_mask_generation_serving/endpoints.example.yaml carries the known-good
entries (the cloud recipe and the shipped local gemma recipe), and copying it to
~/.anomalygen/endpoints.yaml is the whole setup (the file lives outside the repo, so a real key never enters version control or a docker build context).
| # | State | What to do |
|---|---|---|
| 1 | The models resolve in the registry | Use it. An entry (model + base_url + key) per model name is the whole requirement; --gen-model/--judge-model say which entries a run uses. |
| 2 | Anything less | Stop and ask the user — supply an API, let the skill start the model service, or switch to mask_backend=amp. Never decide for them; the two backends do not produce the same masks. |
--gen-model and --judge-model are required flags — there is no default model — and every
name they carry must resolve in the registry: the entry point resolves all of them before it builds
the transport and refuses to start when one does not — there is no environment fallback to
silently reroute a name. The key's value is ignored
by an endpoint that does not check keys — so key: EMPTY (or omitting key:) is enough for an
unlocked local server. The run's opening model routing: log lines say where each role resolved;
they carry only whether a key is set, never its value.
Which endpoint a model name reaches
Exactly two routing layers, answering different questions:
The base picks the envelope. Every base is an OpenAI-compatible endpoint and every call to it goes out as
POST <base_url>/chat/completionswithauthorization: Bearer <key>, whatever the model on it is called — a model namedanthropic/claude-*(OpenRouter's id style) included. The one exception is a property of the world, not configuration: the public Anthropic API (https://api.anthropic.com/v1, the one base constant in the code) serves exactly one protocol — the native/messageswire format — so every model on it takes the Anthropic route, authenticated the way that endpoint requires (x-api-key: <key>+anthropic-version: 2023-06-01, not a Bearer header). Only Claude models live there; a wrong name fails at the capability smoke before the run spends.The model name picks the cache markers. A claude-named model (an id containing
claudeoranthropic) carries the transport'scache_controlbreakpoints whatever base it resolves to: natively on/messages, and embedded in OpenAI block form on/chat/completions— the LiteLLM/OpenRouter gateway convention, which such gateways forward to their Anthropic backends, so prompt caching survives the chat envelope. Every other model gets the clean payload a plain OpenAI-compatible endpoint expects, with no marker anywhere in it.
<base_url> carries the version segment, exactly like the OpenAI SDK's base_url — the transport
appends only the route suffix above, never a /v1 of its own.
A user's own OpenAI-compatible endpoint therefore needs nothing added to this codebase — one
endpoints.yaml entry. Put the provider's documented OpenAI base_url into base_url: verbatim
— the same string their own OpenAI(base_url=...) example uses, whatever version path it carries
(https://.../v1, Gemini's https://generativelanguage.googleapis.com/v1beta/openai, ...) — put
their key in key:, pass their model name to --gen-model/--judge-model, and the call goes out
as POST <their base_url>/chat/completions with authorization: Bearer <their key> — the model
name plays no part in picking the envelope. That payload carries the output cap as
max_completion_tokens and no sampling parameters, which is what OpenAI's own endpoint requires of
a reasoning model such as GPT-6 Astra (it rejects max_tokens and temperature with HTTP 400);
vLLM reads max_completion_tokens into the same field as its deprecated max_tokens, and LiteLLM
gateways translate it per backend — so the shipped cloud model works from https://api.openai.com/v1
directly with nothing but its registry entry (the example file carries it, commented out).
Routing is per model name, so generation and judging can sit on different endpoints in one
command (a local Gemma generating while a cloud judge scores) — each name simply carries its own
entry. When entries for a cloud model and a local service both exist, the model name
decides: the required --gen-model/--judge-model flags name exactly which entry each role
uses — never a detector, so the same command always reaches the same endpoint.
The judge matters more than the generator: a weak judge rates bad masks highly, and every quality-improving option in the engine routes through the judge.
Starting the model service (option b)
Needs a GPU. The weights — google/gemma-4-31b-it, ~62.6 GB, apache-2.0, ungated — are a
checkpoint like every other model: scripts/download_checkpoints.sh gemma fetches them into
checkpoints/hf, and preflight's checkpoint group verifies them. The serving recipe
defaults HF_HUB_CACHE to checkpoints/hf, so a start finds them there.
The gemma argument matters here and nowhere else in this skill: bare, the script provisions all
eleven groups. Named, it fetches this model only.
The service is a Step 2 thing and only a Step 2 thing. Both mask sets — validation and generation
— are Step 2 products, so it is started once, both runs go against it, and it is stopped before the
step ends. It holds a large share of the GPU (--gpu-memory-utilization 0.9), and Step 3's training
and Step 4's SDG need that GPU: leaving it up is how a fine-tune OOMs for a reason nothing in its own
log explains. Starting is idempotent — probe serverctl.sh health first and start only if it is down
— and stopping is part of the step's gate (parent SKILL.md, Step 2).
In a container — the design is ONE container, no sidecar
The shape is one container, no sidecar: vLLM runs as a background process inside the running
container, and AMG in that same container reaches it on plain localhost:8902 — so nothing is
exposed, no second container is orchestrated, and the base URL needs no host name. vLLM lives in the
app venv: vllm shares the image's torch (docker/Dockerfile pins both), so the vllm on
the default PATH is /opt/venv/bin/vllm — the same interpreter as everything else.
Weights reach the service through the repo bind-mount itself: they live in checkpoints/hf with
every other checkpoint (see above), and the recipe defaults HF_HUB_CACHE there — so the documented
docker run needs no weight flags at all: no extra mount, no HF_HOME.
The image and its docker run invocation are owned by the cdf-anomalygen-release skill —
skills/cdf-anomalygen-release/SKILL.md and its references/running.md; take both from there rather
than reconstructing them here. The serve command inside the container is the same recipe as on a dev
host below: the image carries the whole repo at /workspace/cdf-anomalygen,
scripts/agentic_mask_generation_serving/ included. Everything above about when the service runs — Step 2 only,
idempotent start, stop before the step's gate — holds identically in a container.
On a dev host
GPUS=$(nvidia-smi --list-gpus | wc -l)
[ "${GPUS:-0}" -ge 1 ] || GPUS=1 # one device is the floor, not a failure
mkdir -p results
CUDA_VISIBLE_DEVICES="$(seq -s, 0 $((GPUS - 1)))" TP="$GPUS" \
setsid scripts/agentic_mask_generation_serving/gemma-4-31b-it.sh > "results/gemma_server_${NAME}.log" 2>&1 &
scripts/agentic_mask_generation_serving/serverctl.sh wait # 0 up · 1 no such process · 2 still loading, call again
scripts/agentic_mask_generation_serving/serverctl.sh health # /v1/models AND a real completionThe log carries ${NAME} for the same reason the two AMG logs carry ${AMG_MODE}: a fixed filename
means the next run silently overwrites this one's startup record — and that record is the only evidence
of why a start failed, which is exactly what you go looking for when wait reports the process gone.
wait is called repeatedly, like train_control.sh wait. Each call is a ~9.5-minute slice
(--timeout, default 570s) and reports state as its exit code:
| exit | meaning | what to do |
|---|---|---|
| 0 | /v1/models answered | run health, then Step 2 |
| 1 | no vllm serve --port 8902 process | read results/gemma_server_${NAME}.log; it is not coming |
| 2 | process alive, socket not accepting yet | call wait again |
A cold start is 10–15 minutes on one GPU and health reports LOADING for all of it. Measured on a
single RTX PRO 6000 at MAXLEN=65536: weights 352s, torch.compile 96s, CUDA-graph capture 16s, then
chat-template detection — first successful /v1/models about 14 minutes in. Roughly 3.5 of those
minutes came after the log printed Supported tasks: ['generate'], which is emitted before uvicorn
binds the socket. Treat exit 2 as progress, not a stall: restarting on it leaves two vLLMs on one
GPU, each asking for --gpu-memory-utilization 0.9, and the second dies in a way that reads like the
original failure.
TP must equal the number of devices CUDA_VISIBLE_DEVICES names. MAXLEN (default 32768) is
the serving window every request must fit prompt + answer inside. The engine's agent output budget
is a FIXED constant (8000, like every other per-call budget — no env, no registry field), sized so
prompt + budget fit the default window with the measured prompt maximum (~23.4k); a window smaller
than 32768 rejects big prompts — which shows up as whole types stalling on
degraded[api_abort:bad_request] — and the remedy is a larger MAXLEN, never a smaller budget.
setsid because a job backgrounded from an agent's shell is signalled when that shell goes away.
Then run Step 2 against it — the wiring is endpoints.yaml's gemma entry (from the template),
which resolves nvidia/google/gemma-4-31b-it to http://localhost:8902/v1. Serve on any other
port/host, or lock the server (API_KEY=<secret> at startup), and it is the same entry: point its
base_url: at the new address and put the matching value in key::
M=nvidia/google/gemma-4-31b-it
python -m anomalygen.scripts.agentic_mask_generation.generate \
--dataset-path "${DATASET_DIR}" --anomaly-types <texture>+<defect> [...] \
--num-sdg "${NUM_SDG}" --gen-model "$M" --judge-model "$M" \
--beam-multi 4 --beam-pairwise \
--exemplar-captions --exemplar-visual-prompt --coverage-note \
--visual-prompt auto --defect-library auto --out-dir "${AMG_DIR}"Gemma always runs with the in-loop beam on — --beam-multi 4 --beam-pairwise — and with the three information channels open: --exemplar-captions --exemplar-visual-prompt --coverage-note — as above. The
defaults leave both off because a strong proposer's single refine turn is enough; a
smaller local model's is not. --beam-multi 4 asks each refine turn for 4 distinct-strategy fix
options in one call and keeps the best, and --beam-pairwise has the selector compare those options
A-vs-B instead of scoring each alone — the easier judgment for the same model that is also judging.
The three information channels ride with the beam for the same reason — they hand the smaller model what a strong proposer infers on its own, and each defaults OFF so the cloud recipe's prompts stay byte-identical:
--exemplar-captions— each real example travels with its own distill-time caption (what its annotation sits on and how fully it covers it), so the card's one-line summary is never the only reading of the evidence.--exemplar-visual-prompt— each example is also shown as the visual prompt's own painted map with its REAL mask outlined: where annotations live is a picture, not a phrase that can be read against the wrong feature.--coverage-note— the judge, the refiner and the pairwise selector all read the same measured coverage note (the candidate and the real annotations measured with ONE scale-free yardstick), so "how full is full" is a number, not an impression.
Together they cost roughly a quarter more wall-clock per mask; leave them off for cloud models.
scripts/agentic_mask_generation_serving/serverctl.sh stop ends it. The recipe and the control script are
scripts/agentic_mask_generation_serving/; the serving limits and their failure modes — the context window, the
per-prompt image cap, why health asks for a completion rather than trusting /v1/models — are
the "Serving notes" in the tutorial's Prerequisites
(docs/tutorial/texture/3-agentic-mask-generation.md). One of them bears repeating here: stop
matches the port and never the model name (pkill -f gemma also matches the shell that launched
it). One more lives in the recipe instead: it prepends $CUDA_HOME/bin to PATH whenever CUDA_HOME is set, which is how
a vLLM that wants a CUDA toolkit at startup finds one on a host with no system install — export
CUDA_HOME at the bundled toolkit inside the venv vLLM runs from before the launch above.
That last one is a dev-host concern only — skip it in the container. The image ships no CUDA
toolkit binaries at all (/usr/local/cuda carries compat/ and lib64/, no bin/, no nvcc), and
vLLM starts there without one because the recipe disables every FlashInfer path it would otherwise
take — the sampler, and at TP>1 the allreduce fusion pass and all-reduce communicator — since those
kernels are JIT-compiled and need nvcc. Leave CUDA_HOME unset rather than hunting for a toolkit
that was never installed.
Before it spends: the capability probe
Every run sends one call per distinct model in {--gen-model, --judge-model} — two tiny
generated images and a request for {"n_images": <int>} — down the same transport that model will
use for the real work, before the first defect card is distilled. A model that will not take images
or will not answer in JSON fails every call AMG makes, and this is what turns that into a refusal
that costs one call instead of a batch:
degraded[model_smoke_failed]: <model>: <what it could not do>and a non-zero exit whose message names the fix — a credential the endpoint refused, a model that is
not serving images, or a model that would not answer in JSON. --skip-smoke turns the probe off,
which moves that same failure into the middle of the batch.
Cost and time
Defaults are quality-first: the throughput levers (--beam-prune --accept-continue --judge-cache-order) are OFF. Offer them when the user asks for throughput or the batch is large —
on the azure/anthropic/claude-opus-5 measurement below they cut the calls per delivered mask to
roughly a third — and state that they trade measured quality for that saving when you do.
A small per-type share is dearer per mask. Each type pays a fixed setup (defect-card
distillation, valid-region enumeration, visual-prompt precompute) that amortizes over however many
masks the type delivers, and the batch overshoot the trim discards is proportionally larger when the
target is small. Measured on azure/anthropic/claude-opus-5: a 40-masks-per-type run pays ~10
calls per delivered mask (20–30 minutes wall); at 5 masks per type across 5 types it is ~13–15
calls per delivered mask (368 calls for 25 masks, then 324 for another 25), each type overshooting
to 10–14 accepted masks before trimming to 5. Budget accordingly when a run is many types × few
masks rather than few types × many.
Flags that matter (everything else has working defaults)
--num-sdg N— delivered masks for the WHOLE run, split across the types the way AMP splits its own--num_sdg. Total rows = exactlyN. This is whatSKILL.mdStep 2 runs.--mode inference|validation— how that split is made, matching AMP's--mode.inference(default, the generation set): even, and a type may get 0.validation: proportional to each type's real mask count, with a zero share refused — a type absent from the validation set drops out of the per-type KPI that chooses the best checkpoint.--per-defect-counts '{"T+D": 12}'— exact targets per type; omitted types get 0.inferenceonly, and its sum wins over--num-sdg. AMP's--per_defect_countsby another spelling.Omit both
--num-sdgand--per-defect-countsfor one pass over every clean image, with no target at all.--shape flexible|auto|fixed— how each type's mask geometry is made.auto(default): a per-type judgment picks between synthesizing the shape (flexible) and stamping a real training-mask shape (fixed) — except a type whose card says the defect IS a whole host structure, which never stamps (the mask must take its geometry from the target board).flexible/fixedforce one path for every type;fixedwins over the card.--gen-model/--judge-model— REQUIRED, per role; every name carries its own registry entry (--model-registry/endpoints.yaml), and a name with none refuses the run.--out-dir— one run per output set (validation, generation), like AMP's two seeds.--skip-smoke— skip the pre-run capability probe above.--overwrite— destroys the out-dir's masks and starts over. Rarely what you want; see resume.
Resume — interruption is cheap
Re-running the same command into the same --out-dir continues the run: already-delivered masks
are counted, mask filenames never collide, and only the shortfall is produced. A run whose settings
differ from what the out-dir holds is refused with the differing fields named. There is no --resume
flag — matching settings are the switch. This also means a dead endpoint mid-run costs only the
shortfall: the run stops loudly, keeps what it earned, and says how to continue.
The run reports its own deviations — read them
Every time the run does something other than what was asked it logs one line:
degraded[KIND]: detail. Before trusting a batch, summarize them:
for f in "${LOG_DIR}"/amg_*_"${NAME}".log; do
echo "== $(basename "$f")"
grep -o 'degraded\[[^]]*\]' "$f" | sort | uniq -c | sort -rn
donePer log, not merged: the validation and the generation set are separate deliverables, and a clean validation run must not dilute a dirty generation one into looking acceptable.
Frequent kinds and what they mean: json_repaired:* (a truncated model reply was salvaged — benign
in small numbers) · vp_auto_no_card (the run had no visual cue — the score is not comparable;
pass --defect-library auto) · api_abort:bad_request (the model's context cannot fit the request —
on a served model raise MAXLEN) · model_smoke_failed (a model failed the capability probe and the
run stopped before spending) · type_failed (a type delivered nothing; the run says why and exits
non-zero).
spatial_dependency and AMP interplay
AMG does not read defect_spec's spatial_dependency — it decides placement by looking at the
board, per candidate, and the judge enforces it. Every type in --anomaly-types is placed by AMG, so
the run's testcase.jsonl is the whole deliverable. When the field is present, log the notice (parent
SKILL.md has the exact line) and leave the default alone; mask_backend=amp is the supported way
to honour free/text/cad routing.
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: Needs Python
The author says: Requires a CUDA GPU and the built env (native .venv or the Docker image); Hugging Face auth + network access to download the model checkpoints.
Security
What our scanner noticed· Medium risk
- Phrasing that asks for actions without confirmation
- Large base64-encoded blobs
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
- Publisher
- NVIDIA (Official)
- Source
- NVIDIA/skills · skills/cdf-anomalygen · 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 or NVIDIA.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download cdf-anomalygen.zip