npx skills add ...
npx skills add nvidia/model-optimizer --skill evaluation
Evaluates accuracy of quantized or unquantized LLMs using NeMo Evaluator Launcher (NEL). Triggers on "evaluate model", "benchmark accuracy", "run MMLU", "evaluate quantized model", "run nel". Handles deployment, config generation, and evaluation execution. Not for quantizing models (use ptq), deploying/serving models (use deployment), or comparing completed baseline-vs-quantized results (use compare-results).
npx skills add nvidia/model-optimizer --skill evaluation
Guide the user through creating NEL YAML configs, running evaluations, and monitoring progress.
If MODELOPT_WORKSPACE_ROOT is set, use the common skill's workspace-management.md and reuse existing workspaces (this skill is usually the final stage of PTQ → Deploy → Eval; carry any deployment-time patches into deployment.command).
A few agentic AA benchmarks do not run on the currently validated
nemo-evaluator-launcher 0.2.6 path (Steps 1–9 don't apply). They run on nel-next
(nemo-evaluator[harbor] 0.4.x) — a separate package, CLI (nel eval run), -O
overrides, and services/benchmarks/cluster/output schema. If the user asks
for one, do not add it to a 0.2.6 evaluation.tasks list — instead:
references/nel-next.md (shared: venv, schema, AWS creds, architecture, timeout strategy, MLflow, run flow) + the per-benchmark recipe recipes/tasks/aa_next/{terminal_bench_2_1,swebench_verified}.md; start from recipes/examples/example_eval_next.yaml."$SKILL_DIR/scripts/nel-next.sh" --setup-only (keeps 0.2.6 nel untouched).modelopttools:eval-config (Step 3b) to write the AWS-sandbox creds + harbor infra rows (${NEL_NEXT_EVAL_IMAGE}, ${HARBOR_*_ECR_REPOSITORY}) into .env; always include the output.export_config.mlflow block.nel-next.sh eval run), then push to MLflow — SLURM doesn't auto-export, so run nel-next.sh mlflow-push -r <run_id> -c <cfg> after (config-driven; see references/nel-next.md).Steps 1–9 below are currently validated with 0.2.6 — use them for everything else.
simple_agent) path — branch here tooMRCR does run on the currently validated 0.2.6 nel launcher (as a nemo_gym
task, not nel-next), so Steps 1–9 apply — but it is mechanically special and
standalone (one gym eval per config; never mix it with aa/ tasks). It is
simple as gym tasks go: simple_agent, no judge — deterministic prefix-gated
grading, HF_TOKEN the only secret. Not an AA benchmark — never generate it
for an "AA" request. If the user asks for MRCR:
references/gym.md (pinned launcher, gym prepare/reap machinery,
pin↔container coupling, preflight gaps, failure modes) + recipes/tasks/gym/mrcr.md;
start from recipes/examples/gym/example_mrcr.yaml (1M variant, like the golden).config_n3_1m / config_n3_128k / config) — it
sets the context cap, dataset and metric prefix; the three are not
comparable; set it in both data_prep_params and collect_rollout_params..env: HF_TOKEN (dataset + n3 tokenizer are gated) plus
NEMO_EVALUATOR_TRUST_PRE_CMD=1 (the pre_cmd installs tiktoken +
transformers; prepare fails without it) and
NEMO_EVALUATOR_TRUST_UNLISTED_TASKS=1 (nemo_gym is not in the FDF map).container: is ??? and the bootstrap
exits 1 on a non-git /opt/Gym (the public eval-factory/nemo-gym:* images).
NVIDIA-internal: modelopttools:eval-config Step 3d names a working image.--max-model-len 1100000 +
VLLM_ALLOW_LONG_MAX_MODEL_LEN=1, gpu_memory_utilization: 0.95,
multi-instance fan-out); never cap output tokens; report the needle-count
strata alongside pass@1/accuracy."$SKILL_DIR/scripts/nel-gym.sh"; it
enforces the currently validated 0.2.6 launcher even if nel on PATH is stale
and avoids an unset NEL_INVOCATION_ID failure before client startup.
limit_samples is inert on the gym path — canary with the gym's own
++limit=N (see the recipe's Canary section), remembering the prepare pass
still runs in full.SSH: NEL opens its own connection as a bare
ssh <user>@<host>with no-i, so a non-default key filename makes every submit fail withPermission denied (publickey,password)even though your own tooling works. Add anIdentityFileentry to~/.ssh/configper cluster and verify withssh -o BatchMode=yes <user>@<host> true.
Config placement gotchas, each of which costs one failed submission:
sbatch_commentbelongs underexecution:, notcluster:(otherwise inert, and the idle-GPU reaper kills the job); pyxis needsregistry#path:tagfor non-DockerHub images or it prependsdocker.ioand 404s; NEL rejects file mounts ("Mount paths must be directories") — put file overrides inpre_cmd.
Run nel --version; if missing, instruct pip install nemo-evaluator-launcher. If user has an existing config, skip to Step 8 (optionally review for ??? and quantization flags first).
Set up .env now (not Step 8). The working .env lives at the workspace root — the directory you run nel from — matching modelopttools:eval-config's convention; do not create it under the skill dir. (NEL does not discover .env by path: it reads secrets from the shell env via the host: prefix after you source, so the location is purely which file you source before nel run. Keeping the single .env at the workspace root avoids a stale duplicate under the symlinked, shared .agents/ skill tree.) For judge-scored / user-sim tasks (HLE, AA-LCR, Tau2), seed it from the template if absent — the template ships under the skill dir, the working .env does not: [ -f .env ] || cp "$SKILL_DIR/recipes/env.example" .env. Then try modelopttools:eval-config (if available) to fill the judge model_id/url rows (user adds the secret key). Needed before Step 5, which substitutes those values into task <VAR> placeholders.
Secret safety — never open .env with Read/Write/Edit. The harness mirrors later edits of any agent-opened file into the transcript, so touching .env leaks the keys the user adds afterward. Use shell only (cp to create, source to load — neither echoes); edit env.example, never .env; leave value entry to the user / modelopttools:eval-config.
Task recipes (always read before editing the relevant task in the config):
AA Index v2 suite (default for quantized-checkpoint validation, see references/quantization-benchmarks.md): recipes/tasks/aa/{gpqa_diamond,hle,lcr,scicode,ifbench,mmmu_pro,tau2_bench_telecom,omniscience}.md
Optional: recipes/tasks/mmlu_pro.md, recipes/tasks/aime_2025.md, recipes/tasks/livecodebench.md
nel-next only (different evaluator — see the nel-next section below, NOT the 0.2.6 steps): shared reference references/nel-next.md + per-benchmark recipes recipes/tasks/aa_next/{terminal_bench_2_1,swebench_verified}.md (agentic). The aa_next/ dir holds tasks that require nemo-evaluator[harbor] 0.4.x (the package; nemo-evaluator-next is the eval image repo); aa/ is the 0.2.6 suite.
NeMo Gym tasks — recipes/tasks/gym/*.md, with self-contained examples at recipes/examples/gym/example_<task>.yaml, over the shared reference references/gym.md. The gym/ dir groups by harness (0.2.6 nemo_gym), not by suite membership, so read AA membership per task from the table below — never from the path. Every gym task is standalone: generated as its own config from its example, one gym eval per config, never merged into the aa/ multi-task tasks list and never mixed with each other.
| Task | Recipe / example | In AA suite? | Generate when |
|---|---|---|---|
| MRCR (simple agent, long-context) | recipes/tasks/gym/mrcr.md, recipes/examples/gym/example_mrcr.yaml | No | only when the user asks for MRCR by name, or for long-context coverage |
GDPVal is no longer supported by this skill — its recipe, example config and SIF tooling were removed. If a user asks for it, say so and offer the aa/ suite instead; do not reconstruct a GDPVal config from an older copy of this skill.
AA rule: If the user mentions "AA" / "Artificial Analysis", generate the recipes/tasks/aa/ tasks as one multi-task config. Do not add MMLU-Pro, AIME 2025, or LiveCodeBench unless explicitly asked. SciCode needs at least 8 submissions, not one, reported as their mean (recipes/tasks/aa/scicode.md). The suite this skill generates omits GDPVal, so an aggregate built from it is not directly comparable to a published AA Index that includes GDPVal — report per-task scores.
Shortcut path (when task list is known up front, e.g. "run AA"):
recipes/examples/example_eval.yaml as the base.evaluation.tasks, applying any per-task notes.example_eval.yaml: (a) the trigger execution.auto_export.destinations: [mlflow] (without it the run is not uploaded), and (b) the export.mlflow block that configures it. In the export.mlflow block use literal values for experiment_name / description / tags — substitute the actual served_model_name and sampling params. Do not use ${deployment.*} / ${evaluation.*} cross-references: with auto-export on, NEL resolves the export block at submit time in a scope without those nodes and fails with Interpolation key '...' not found (${oc.env:USER} and ${oc.env:MLFLOW_TRACKING_URI} are fine — they're env vars). Because these literals can't interpolate, keep the temperature / top_p / max_new_tokens tags equal to the top-level params and update both in the same edit — they're the only queryable record of sampling in MLflow (NEL doesn't log them as run params), so a stale tag silently misreports the run. tracking_uri = ${oc.env:MLFLOW_TRACKING_URI} from modelopttools:eval-config (not hand-filled), and auto-export needs execution.cpu_partition (e.g. gcp-nrt cpu) — it's a separate CPU-only sbatch that GPU-only partitions reject (Cannot find GPU specification), silently dropping the link. Before filling experiment_name/tags, read the checkpoint's .experiment.json (Step 3) and carry the PTQ run's experiment name plus its modelopt_* tags across (Step 4).Ask the 5 questions via AskUserQuestion (categories must match nel skills build-config --help — run that first to confirm the current option names; CLI options override this list).
Build the base:
(--output omitted = cwd auto-named; directory = dir + auto-name; *.yaml = exact path. Never overwrites.)
Model path. Checkpoint path (/, ./, ../, ~, or exists on disk) → set deployment.checkpoint_path, leave hf_model_handle: null. Else HF handle (one /, not on disk) → set deployment.hf_model_handle, leave checkpoint_path: null.
Read its ModelOpt provenance now — hf_ptq.py --mlflow leaves .experiment.json in the
checkpoint it wrote, and having the values in hand saves revisiting this at Step 4:
It is a tracking pointer, not a quantization signal: read it independently of the quant
detection below and never infer deploy flags from it either way. A checkpoint can carry one
and be quantized by something other than ModelOpt; a ModelOpt checkpoint quantized without
--mlflow carries none.
NEVER point
checkpoint_pathat a HuggingFace cache snapshot dir. Entries undersnapshots/<sha>/are relative symlinks into../../blobs/. NEL mounts only the snapshot dir at/checkpoint, so every link dangles in-container and vLLM dies withInvalid repository ID or local directory specified: '/checkpoint'. Pre-staging intoHF_HOMEdoes not help — that works forfrom_pretrained, not a mounted directory. Build a hardlink farm (same filesystem, no extra space) and point at that:(
SNAP/DESTmust be exported — they are read inside a new shell — and the loop preserves nested paths;basenamewould flatten subdirectories into one level.) The same applies to ModelOpt exports whose--source_ckptwas a snapshot: the exporter preserves symlinks, shipping a danglingtokenizer.json. Check withfind "$OUT" -type lbefore serving.Prefer
checkpoint_pathoverhf_model_handleon SLURM —hf_model_handleisn't reliably mounted at/checkpoint, so the deploy dies withHFValidationError. To eval an un-staged HF model, stage it first (huggingface_hub.snapshot_download) and pointcheckpoint_pathat it. Seeexample_eval.yamlfor why.
Auto-detect ModelOpt quantization (checkpoint paths). Check config.json for quantization_config (or legacy hf_quant_config.json):
--quantization flag by default — vLLM auto-detects from quantization_config / hf_quant_config.json. Add only when the card, vLLM version, or dry-run error requires it.--quantization modelopt_fp8 / modelopt_fp4 / modelopt — verify against installed version.Some models need extra vLLM backend env vars (model-card research) — e.g. VLLM_NVFP4_GEMM_BACKEND=marlin (Nemotron Super), or VLLM_USE_FLASHINFER_MOE_FP4=1 + VLLM_FLASHINFER_MOE_BACKEND=throughput (NVFP4 MoE, e.g. NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4). Put them in deployment.env_vars (not command) with the lit: prefix (VLLM_USE_FLASHINFER_MOE_FP4: lit:1); see example_eval.yaml and Step 5's prefix rule.
Auto-detect from config.json:
| Field | Flag |
|---|---|
max_position_embeddings | --max-model-len <value> |
auto_map exists | --trust-remote-code |
Source 1 — recipes.vllm.ai/<org>/<model> (curated vLLM recipes; authoritative for parallelism, family-specific flags like --reasoning-parser / --tool-call-parser / --mm-encoder-tp-mode, vLLM version, spec-decoding, GPU count). Fetch the page for the EXACT model id, not a base/sibling — variant minimums differ (e.g. MiniMax-M2 ≥0.11.0 vs M2.7 ≥0.20.0). Pin variants via query params (e.g. ?variant=fp8&strategy=single_node_tep).
WebFetch caveat — triage the summary:
- "No
vllm servecommands found" / "page is a usage guide": JS-rendering miss. recipes.vllm.ai pages always have ≥1 command. Ask the user to paste it or share the variant URL.- Single recipe returned for a model with known multiple variants → retry with variant-pinned URL. Axis names differ per model (Qwen:
?variant=&strategy=; Kimi:?advanced=; others vary — no fixed pattern).- Variant label contradicts the command (e.g. label "TEP" but command shows DP+EP) → summarizer conflated variants; ask user.
For non-trivial deployments (≥120B, multi-node, novel arch), ask the user which variant before fetching.
Source 2 — HF model card + config.json (authoritative for):
| Signal | Flag |
|---|---|
max_position_embeddings | --max-model-len <value> |
auto_map | --trust-remote-code |
| Reasoning/CoT documented | --reasoning-parser (and --reasoning-parser-plugin if custom) |
| Tool-calling documented | --enable-auto-tool-choice --tool-call-parser <parser> |
| Custom flags in card | Add as specified (e.g. --mamba_ssm_cache_dtype float32) |
Cross-check rules:
hermes → Qwen3 qwen3_coder), recipe-only flags like --language-model-only, ARM64-specific card notes.command: fieldRewrite the build-config output into one command: field. Move all parallelism (--tensor-parallel-size, --data-parallel-size, --pipeline-parallel-size) into the command; do not keep separate tensor_parallel_size / data_parallel_size / extra_args YAML fields.
Conventions: always start vllm serve /checkpoint (NEL mounts here); always --served-model-name ${deployment.served_model_name} (required; see example_eval.yaml for why); always --host 0.0.0.0 --port ${deployment.port}; use folded scalar (>-) for one flag per line. Example fallback --max-model-len 131072 covers AA-LCR (~120K + 16K gen) and SciCode (≥ 65536) — prefer config.json / recipe value.
For how to choose --tensor-parallel-size / --data-parallel-size / --pipeline-parallel-size (and EP) from the model size and your GPU count, read references/parallelism.md — cross-check the layout against recipes.vllm.ai, then adapt to the GPUs you actually have via the fit math there.
Image / vLLM version. Treat default image: vllm/vllm-openai:v0.26.0 as a floor to verify: bump to the exact model's recipes.vllm.ai minimum if higher. Running below minimum is a trap — the server starts, then a worker dies mid-inference with CUDA error: an illegal memory access, easy to misread as a kernel bug. A model newer than the latest release may have no numbered tag — use the image its recipe names. Never :latest (breaks reproducibility). Surface version bumps to the user.
NVFP4 on Blackwell B300/GB300 (sm_103) needs a CUDA-13 build — the cu12 build has no sm_103 FP4 kernel, so engine init dies with
CUDA error: no kernel image is available. Pick the tag by the CUDA version it reports, not by its name — vLLM inverted its tag convention at v0.20.0:
vLLM version CUDA-13 tag CUDA-12 tag ≤ v0.19.x suffixed -cu130unsuffixed ≥ v0.20.0 unsuffixed suffixed -cu129v0.20.0 ships both suffixes; after it
-cu130doesn't exist, so asking for it yields a missing tag. Select a tag whose config blob reportsCUDA_VERSION≥ 13 (registry API), reading the child manifest for the platform you deploy on (arm64 Grace/GB300, amd64 x86) —TORCH_CUDA_ARCH_LISTdiffers per platform, so check your arch against that child. Multimodal on sm_103 may also need--mm-encoder-attn-backend TRITON_ATTN. Full note inrecipes/examples/example_eval.yaml.
Silence is not contradiction. Drop/override only when the recipe sets a different value for the same setting (e.g. recipe pins --max-num-batched-tokens 16384 → use 16384).
--model-loader-extra-config '{"enable_multithread_load": true, "num_threads": 128}' — parallelizes checkpoint load, the single biggest deploy-time cost for large checkpoints. A big MoE otherwise loads shards ~sequentially (~1 min/shard → e.g. ~40 min for a ~450 GB / 45-shard checkpoint); on a preemptible queue that long load window is exactly where jobs get killed before they ever serve. num_threads defaults to 128; scale it to the checkpoint (smaller for small models, bounded by the shared-FS read bandwidth — too high yields no gain). Safe to always include.
--max-num-batched-tokens 8192 — caps per-step batched tokens; prevents long-prefill stalls.
--enable-chunked-prefill — interleaves long prefills with decode steps (required for AA-LCR's ~120K input). Modern vLLM defaults this on for many models; set explicitly to avoid drift.
--enable-expert-parallel — MoE-only default. Detect MoE from handle suffix (-A10B, -A3B, etc.), num_experts / num_local_experts / n_routed_experts in config.json, or card. No-op when TP=DP=1, safe to always include for MoE. Do not add for dense models. See references/parallelism.md for what EP does and the DP-attention + EP-MoE throughput pattern.
--max-num-seqs N — omit at generation time (top-level parallelism is ???). Add this comment above command::
In Step 4 compute and append. Example: top-level=16, Tau2=128, DP=8 → ceil(128/8)=16. Too small → request queuing; too large → wasted KV reservation. For how to choose the parallelism it derives from, read references/parallelism.md.
The top-level nemo_evaluator_config.config.params must contain exactly these six fields — no top_k / presence_penalty / repetition_penalty / min_p:
Per-task max_new_tokens overrides are forbidden — set one top-level ceiling everywhere.
Cross-check temperature / top_p / max_new_tokens against references/nvfp4-modelcard-sampling.md — the published settings for the 2026 NVFP4 checkpoints under huggingface.co/nvidia that disclose them (older releases and cards that publish nothing are absent — for those, read the card; -DSpark / -DFlash spec-decode variants share their base checkpoint's row, since spec decoding does not change the target's output distribution). The card is the source of truth; this file is a reference, not a constraint — use it to confirm a value you read, to fill a gap when the card is silent or ambiguous, and to catch a misreading. Worth consulting whenever the model is an NVFP4 checkpoint or shares a family with one (Qwen3.x, GLM-4.7/5.x, Kimi K2.x/K3, MiniMax M2.x/M3, DeepSeek V3.x/V4/R1, Gemma 4, Nemotron 3/3.5, Llama-Nemotron, Mistral Medium 3.5), and especially when you are unsure. It is a dated snapshot, so for anything newer than it, trust the card. See that file's "Lookup" section.
temperature / top_p are different: per-task overrides ARE allowed and often required. Cards often specify sampling per scenario — DeepSeek-V4-Pro-0813 gives top_p = 0.95 for agentic scenarios and 1.0 otherwise, so a single top-level 0.95 is wrong for every non-agentic task.
Set the top-level value for the majority case, override only the tasks the card calls out, and apply
the split identically to baseline and candidate. The export.mlflow tags record only the
top-level values, so note any per-task override in the run description — otherwise the
overridden task is reported under sampling params it did not use.
max_new_tokens — mandatory model-card lookupmax_tokens / max_new_tokens / "output length" recommendation. Pick the highest value the card mentions (Qwen3.6: 32768 general + 81920 math-coding → use 81920). Annotate with a citing comment.
Card figures are SINGLE-TURN. On multi-turn / agentic benchmarks the model's own answer is fed back in, so the cap must satisfy n_turns × max_new_tokens + prompt < max_model_len. Taking a card's headline "384K output" literally lost SciCode samples to HTTP 400; 65536 was clean. (references/run-validation.md already covers checking finish_reason: length after a run.)references/nvfp4-modelcard-sampling.md as a reference. Listed and in agreement → proceed with confidence. Listed and different → the card wins; re-read it, then note the discrepancy for the user rather than auto-correcting either way. Not listed, or the card is silent or ambiguous → take the nearest same-family rows as the value, a far better prior than the generic fallback below. Its max_num_tokens column records the card's headline cap, so rule 2 above still governs: when a card names more than one cap, the highest wins even if that exceeds the row.max_new_tokens: <generic_default> with a "card not yet checked" comment. Either fetch and apply, or fetch and confirm silence.--max-model-len > prompt + max_new_tokens (else generation is silently clipped — AA-LCR's ~120K input leaves little room). Treat such tasks as low-confidence.For quantized checkpoints, read references/quantization-benchmarks.md for sensitivity rankings and recommended sets; present and ask which to include. Read references/model-card-research.md for the full extraction checklist (sampling, reasoning config, ARM64, pre_cmd, output length — see the dedicated bullet there).
Reasoning models: prefer reasoning mode (highest scores). For lower variance / cost / apples-to-apples vs non-reasoning baselines, also consider a non-reasoning companion run.
use_reasoning)The adapter_config block in example_eval.yaml controls request/response
logging and reasoning handling. use_reasoning: true strips the model's
reasoning/CoT trace before scoring (grade only the final answer). Set per type:
use_reasoning: false and drop the chat_template_kwargs
thinking block (no trace to strip; can mangle plain responses).use_reasoning: true, especially when the deployment sets
--reasoning-parser (vLLM emits a separate reasoning channel to strip).use_reasoning: true +
force the thinking flag in chat_template_kwargs). For the exact toggle key
(it drifts across generations) and the reasoning-effort policy, see
references/model-card-research.md → "Reasoning config".Predefined per-cluster execution config (check FIRST). Some installs ship internal/slurm/<cluster> execution groups (optional nemo_evaluator_launcher_internal pkg) that pre-fill hostname/partition/gres — leaving only account/output_dir/walltime. Discover at runtime (nothing cluster-specific hardcoded):
Hostname match → set defaults: - execution: internal/slurm/<cluster>, drop the redundant execution.hostname (keep account/output_dir/walltime), verify with --dry-run. Else keep slurm/default and fill hostname/account/output_dir manually.
On SLURM, several deploy/eval failures are invisible to --dry-run and only surface at canary (mount_home, HF cache, cpu_partition, top-level vs per-stage env_vars) — read references/slurm.md.
Find every ??? left. Ask the user only for what can't be inferred (SLURM hostname/account/output_dir, the cpu_partition for auto-export, etc.). Don't propose defaults; let them give plain text. (tracking_uri is not one of these — it's ${oc.env:MLFLOW_TRACKING_URI} from modelopttools:eval-config.)
parallelism — size it yourself from the run shape (total requests = dataset_size × repeats vs GPU serving capacity), and set --max-num-seqs to match. Read references/parallelism.md for the decision rule and worked examples; only ask the user if a non-GPU cap (e.g. judge rate limit) is unknown.
Ask about other defaults they may want to change (partition, walltime, MLflow tags).
ModelOpt provenance. When Step 3 found a .experiment.json, carry it into
export.mlflow so evals group under the run that quantized the checkpoint:
.experiment.json field | goes to |
|---|---|
experiment_name | experiment_name, verbatim — replaces ${oc.env:USER}/CHANGEME-served-model-name |
run_name / run_id / run_url | tags modelopt_run_name / modelopt_run_id / modelopt_run_url |
tracking_uri, experiment_id | nothing — both are local to the PTQ's server |
Skip any imported value containing ${, experiment_name included — quoting does not
stop OmegaConf resolving it, and one pass resolves the whole block, so a crafted file could
interpolate an env var into the config. (hf_ptq sanitizes only the experiment name it
derives itself; an explicit --mlflow_experiment reaches the file as typed.) Drop that tag
outright — for experiment_name, fall back to the usual default — and say which you
dropped when you report the run. Otherwise quote the tag values (a bare 20260910 becomes
a date), keep the modelopt_ prefix (untagged, they read as this eval's own run), and
leave description naming the model and sampling params as the template does. Carry the
values verbatim, but flag any that look like placeholders rather than quietly publishing a
run_url with no run behind it.
tracking_uri stays ${oc.env:MLFLOW_TRACKING_URI}. When it differs from the file's —
the usual case — the export creates a same-named, empty experiment on the eval server
under a new server-local experiment_id; the PTQ run is not in it, and only
modelopt_run_url reaches it. Say so when you report the run. To find these evals again
later, query this server for tags.modelopt_run_id = '<ptq_run_id>'.
execution.gres — auto-set if you used a predefined internal/slurm/<cluster> config (above). On the slurm/default fallback it's gpu:8, so set it to the node's GPU count (and match --data-parallel-size/--tensor-parallel-size) or sbatch rejects the job with "Requested node configuration is not available" (e.g. 4-GPU GB300 → gres: gpu:4; check with sinfo -o '%P %G').
Walltime cap: 4 hours. Always execution.walltime: "04:00:00". The cluster does not schedule jobs longer than 4h — this is a hard limit, not a preference.
Evals that exceed 4h of wall-clock time are handled by NEL's built-in dependency-chain resume, not by shrinking the eval. NEL submits the first SLURM job; if it hits walltime, a dependent follow-on job resumes from the response/result caches the first job wrote, then queues another follow-on. Long evals continue across walltime windows automatically. See references/run-validation.md#nel-timeout-and-resume-behavior for the full mechanism.
Never scancel a wedged run to free the GPUs. The dependency-chain resume above fires only on a genuine walltime timeout; afternotok explicitly refuses a predecessor that finished CANCELLED by <uid> and exits in seconds, discarding the response cache the run had already filled. Let it hit the wall clock instead — one scancel threw away ~59 GPU-h of completed generation.
Preemption / external kill — resume manually with sbatch run.sub. On a preemptible account (common on busy internal clusters) the scheduler can CANCEL a run mid-eval for a higher-priority job — sacct -j <id> shows CANCELLED by <uid> (a svc-* service account) with Elapsed well under the 4h walltime. NEL does not auto-resume this (its dependency chain only fires on a genuine walltime timeout). But the run.sub that NEL generated for the job (in its run dir) is re-submittable and resumes from the same output_dir + response cache (skip_filled), continuing from the partial output rather than restarting:
Re-submit again if it's preempted again — each resume re-deploys, then skips already-generated samples, so progress is cumulative across attempts until it completes. Always confirm via sacct -j <id> that the prior job was CANCELLED (not a real failure) before resuming.
Implications for the agent:
num_repeats, split heavy tasks (AA-LCR, SciCode) into separate configs, or otherwise carve up the eval to fit inside 4h. Let NEL chain. (SciCode's mandatory 8+ submissions are independent scored runs, not a walltime workaround.)nel status / nel info and the dependent job's logs before declaring failure. references/run-validation.md covers what a real failure looks like vs an expected resume event.data_parallel_size / parallelism to finish faster is fine when the goal is wall-clock latency, not a walltime workaround — but it's optional, not required, for runs longer than 4h.Tell user: "Run nel ls tasks for the full task list."
For any task with a recipes/tasks/ reference, read it and prefer its YAML fragment + repeat counts.
Ask about add/remove/modify. Per-task overrides under task's nemo_evaluator_config.config.params:
Apply, show updated list, ask "Final, or more changes?" Loop until confirmed.
Tasks that call an external judge / user-simulator / scoring endpoint. Treat this as a general pattern, not a fixed list — HLE, AA-LCR, and Tau2 need one today, but other benchmarks may too (check each task's recipe). Their model_id / url are config, not secrets: substitute the literal values the user keeps in .env (keys per the task's recipe + recipes/env.example) into the task's <VAR> placeholders. Do not emit ${oc.env:...} for these (it silently fails unless the var was exported with set -a). Only api_key stays an env-var name (e.g. INFERENCE_API_KEY), exported and read by the harness. All judges + user-sims (HLE, AA-LCR, Tau2, AIME) use one INFERENCE_API_KEY against one OpenAI-compatible host (AIME's simple-evals default judge endpoint is overridden to it — see its recipe). Get the *_MODEL_ID/*_URL values via modelopttools:eval-config (see Step 1) rather than guessing a host; only fill them by hand if it's unavailable. .env should already exist (Step 1) — if not, set it up now (don't defer to Step 8) before substituting.
Known issue — nemo-skills self-deployment: If using nemo_skills.* tasks (ns_*) with self-deployment (vLLM/SGLang/NIM), you need both of these:
api_key_name only names the env var; the nemo-skills client hard-fails if that var has no value inside the eval container (ValueError: api_key_env_var=DUMMY_API_KEY but the value is not set). On SLURM, a shell export DUMMY_API_KEY=dummy (Step 8) does NOT propagate into the container — NEL only injects vars declared in env_vars. So declare DUMMY_API_KEY: lit:dummy under evaluation.env_vars (note the lit: prefix — see below). The shell export only helps for local/Docker runs. External-deployment configs already define api_key_name.
NEL env-var value prefixes (required): every value in an env_vars map needs an explicit prefix — host:VAR (read from the submitting shell's env at submit time), lit:value (literal string), or runtime:VAR (read in the job at run time). A bare value (e.g. DUMMY_API_KEY: dummy) hard-errors: "Env var value '…' must have an explicit prefix." Use lit: for constants like DUMMY_API_KEY and VLLM_* backend selectors, host: for secrets like HF_TOKEN / INFERENCE_API_KEY.
For models > ~120B or higher throughput needs, read references/multi-node.md for HAProxy multi-instance / Ray TP/PP / combined patterns.
Direct user to https://docs.nvidia.com/nemo/evaluator/latest/libraries/nemo-evaluator/interceptors/index.html. Do not provide generic interceptor info — read the specific interceptor's page if asked, then configure via evaluation.nemo_evaluator_config.target.api_endpoint.adapter_config (target is a sibling of config, not nested under it). Use the per-field syntax from the CLI Configuration section, not a full interceptors: list (that overrides the default chain).
Errata: Logging field names are max_logged_requests / max_logged_responses (NOT max_saved_* / max_* as some docs show).
Default images:
| Framework | Image | Registry |
|---|---|---|
| vLLM | vllm/vllm-openai:v0.26.0 (bump per recipe; never :latest) | DockerHub |
| vLLM (NVFP4 on B300/GB300) | default is already CUDA-13; for older pins see Step 3 | DockerHub |
| SGLang | lmsysorg/sglang:latest | DockerHub |
| TRT-LLM | nvcr.io/nvidia/tensorrt-llm/release:... | NGC |
| Eval tasks | nvcr.io/nvidia/eval-factory/*:26.03 | NGC |
NVFP4 checkpoints on B300/GB300 (sm_103) need a CUDA-13 image — CUDA-12 builds lack sm_103 FP4 kernels. The tag spelling depends on the vLLM version (Step 3 table); verify
CUDA_VERSIONin your platform's child manifest.
Public images → submit without preflight. Private/restricted → check credentials:
Add credentials per the common skill's slurm-setup.md §6 if missing. If you can't add, switch to a compatible public image (e.g. nvcr.io/nvidia/vllm:<YY.MM>-py3 — check catalog.ngc.nvidia.com). Do not retry more than once after an auth failure.
Run directly when the user asked to launch; otherwise ask before submitting.
Env setup: .env is normally already created and filled back in Step 1 (via modelopttools:eval-config), at the workspace root — the dir you run nel from, not under the skill dir. Ensure it exists and source it — do not clobber an existing .env:
Step 8.1 — Dry-run (config validation):
Fix unresolved ???, bad Hydra overrides, missing env vars, invalid mounts, image issues, sbatch errors, obvious deployment errors before proceeding.
Dry-run does NOT validate the image/vLLM version (image pulled only at deploy). Confirm
image:≥ the exact model'srecipes.vllm.aiminimum (Step 3) before submitting — too-old passes dry-run, then crashes mid-inference.
Non-fatal noise: "Failed to get manifest"/
401/404, "Could not extract frame definition file", "proceeding with minimal task definition", "Found N unlisted task(s)" — expected forns_*/recipe tasks and private (gitlab) containers; the task still runs in-container. SetNEMO_EVALUATOR_TRUST_UNLISTED_TASKS=1. Real blockers: unresolved???, interpolation errors, bad mounts, sbatch rejections.
Step 8.2 — Canary (limited-samples, validates everything dry-run can't):
Catches judge auth/rate-limits, container failures, sandbox issues, OOM, bad request formatting, low evaluated counts. Always inspect logs:
Canary each risky task class separately (judge-scored, code-execution, model-only). Start parallelism conservatively; raise only after judge/sandbox logs are clean — they bottleneck before the model. For capacity-bound runs, tune parallelism/--max-num-seqs here against vLLM's reported max concurrency + preemption — see references/parallelism.md.
Single-task rerun: nel run --config <path> -t <task_name> (combine with -o ++...limit_samples=10 for canary).
Step 8.3 — Full run (after canary passes):
Remove limit_samples overrides; keep canary-validated parallelism. If the canary fails, fix and rerun the canary — don't skip to full.
Monitoring: Register the job per the monitor skill for cross-session tracking. One-off live status / debugging → launching-evals skill. Past-run MLflow queries → accessing-mlflow skill. NEL timeout/resume → read references/run-validation.md before treating the run as failed.
Before pulling/reporting scores, validate the run. Read references/run-validation.md for NEL timeout/resume behavior, completed-run validation, diagnostics, and score harvesting. For a baseline that will be compared with a candidate, also perform its External Baseline Sanity Check before a success verdict, then hand the validated runs to compare-results for baseline-vs-candidate deltas.
Issues: https://github.com/NVIDIA-NeMo/Evaluator/issues · https://github.com/NVIDIA-NeMo/Evaluator/discussions*