Kermt Infer
NVIDIAOfficialPublished by NVIDIA
- Skill
- Workflow
- Science and research
- Apache-2.0
- Medium risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The kermt-infer skill provides a structured, containerized workflow for running molecular property predictions using finetuned KERMT checkpoints. It automates the end-to-end process, including system validation, checkpoint verification, data cleaning, RDKit feature generation, and model execution. By enforcing strict validation steps, it ensures that only compatible finetuned checkpoints are used, preventing common errors associated with pre-trained models or malformed input data.
What it does
- Validates that the provided checkpoint contains necessary task FFN heads.
- Cleans input SMILES data and generates RDKit 2D features.
- Executes model inference within a controlled, GPU-accelerated container environment.
- Generates a comprehensive run manifest including command replay and configuration details.
- Provides clear error reporting for data or checkpoint compatibility issues.
When to use it
- When you need to perform inference on a batch of molecules using a previously finetuned KERMT model.
- When you want to ensure data consistency and feature normalization before running predictions.
- When you require a reproducible, logged execution of a molecular prediction task.
- When you need to verify if a checkpoint is ready for inference or requires further finetuning.
Advantages
- Automates complex data preparation and validation steps.
- Ensures reproducibility through detailed run manifests and command replay capabilities.
- Prevents runtime failures by pre-validating checkpoints and input CSVs.
- Maintains integrity of the original checkpoint by using symlinked execution paths.
Usage examples
- “Run inference on my_molecules.csv using the finetuned checkpoint at ./models/my_model.ckpt”
- The skill validates the checkpoint and CSV, prepares the features, runs the inference in the container, and returns the path to the predictions CSV.
- “Perform inference with a batch size of 64 using my_model.ckpt”
- The skill executes the inference workflow using the specified batch size, overriding the default configuration.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/nvidia/kermt-infer/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip kermt-infer.zip -d .agents/skills
Documentation by NVIDIA
kermt-infer
Run predictions with a finetuned KERMT checkpoint on a SMILES-only CSV. The skill is the workflow orchestrator: validate ckpt, validate CSV, prepare data, launch the runner blocking, return the predictions CSV.
Skill and runtime paths
Set SKILL_DIR to the absolute path of this installed skill directory. Export
KERMT_REPO as the absolute path to the KERMT checkout used for model
execution. The bundled container helper mounts that checkout at
/workspace and this skill at /skill (read-only). Commands inside
the container use /skill/scripts/; defaults are bundled in config/.
Hardware requirements
GPUs: 1 (single-GPU). Multi-GPU inference is not currently supported.
VRAM: ≥ 4 GB for the default
batch_size 32.Disk: a few hundred MB per run (cleaned CSV + features + predictions).
Driver / CUDA: any host supporting CUDA 12.6 (the kermt image base).
Inputs
Required:
--ckpt <path>— finetuned checkpoint (must have task FFN heads). The validator refuses pretrain ckpts with a redirect tokermt-finetune.--csv <path>— SMILES-only CSV. First column issmiles; other columns are ignored.
Optional:
--batch-size N— override the configured default (32).--seed N— random seed for inference (deterministic featurization paths).--gpus 0— single GPU id (default 0). Multi-GPU rejected.--from-prepare <dir>— skip the prepare step and reuse an existingprepare_data.jsonin<dir>.
Workflow
Let $KERMT_REPO be the path to your kermt repo checkout, and assume
kermt-setup has built kermt:latest.
Pre-flight: ensure container + system probe.
"$SKILL_DIR/scripts/kermt_container.sh" check_systemRefuse to proceed on
ok: false.Compute run directory.
RUN_DIR=$KERMT_REPO/runs/infer_$(date -u +%Y-%m-%dT%H-%M-%SZ)Validate the checkpoint.
"$SKILL_DIR/scripts/kermt_container.sh" run --ckpt <user-ckpt> -- \ "python /skill/scripts/check_checkpoint.py --mode inference --ckpt /ckpt"Parse the JSON. Abort on
ok: false. The validator rejects pretrain ckpts (has_task_ffn: false) with a redirect tokermt-finetune.Validate the data.
"$SKILL_DIR/scripts/kermt_container.sh" run --data <user-csv> -- \ "python /skill/scripts/check_data.py --mode inference --csv /data/<basename>"Abort on
ok: false.Prepare the data.
"$SKILL_DIR/scripts/kermt_container.sh" run --data <user-csv> --run-dir $RUN_DIR -- \ "python /skill/scripts/prepare_data.py --mode inference \\ --csv /data/<basename> --out /runs/data"Outputs land at
$RUN_DIR/data/prepare_data.jsonwithclean_csv+clean_npzpaths (rdkit_2d_normalized features).Launch the runner (blocking).
"$SKILL_DIR/scripts/kermt_container.sh" run \\ --ckpt <user-ckpt> --run-dir $RUN_DIR -- \\ "python /skill/scripts/run_inference.py \\ --ckpt /ckpt \\ --prepare-manifest /runs/data/prepare_data.json \\ --out /runs \\ [--gpus 0 --batch-size N --seed N]"Returns the predictions CSV path on success.
Report to the user. Output a short summary:
Predictions:
$RUN_DIR/out/predictions.csv(smiles + per-target columns)Manifest:
$RUN_DIR/run.json(cmd_replay + image digest + applied args)Log:
$RUN_DIR/logs/inference.logRow count: <N> molecules predicted across <K> targets
Hard rules
Never modify the user's ckpt. The runner symlinks the ckpt into a unique
<out>/ckpt_link/subdir somain.py predict --checkpoint_dirpicks it up; the source file stays untouched.Arch comes from the ckpt, never from CLI/defaults. The runner records the validator's arch block in
run.jsonbut does not pass arch flags intomain.py predict— predict reads them from the loaded ckpt's saved_args.Single-GPU only. Multi-GPU inference is not currently supported.
Echo applied defaults. The
args_appliedfield ofrun.jsonrecords every flag's value + source (user / default-config). Surface a short summary of any default-filled flag.
Common errors
inference requires a finetuned ckpt with task FFN heads→ ckpt is a pretrain ckpt; usekermt-finetunefirst.prepare_data manifest reports ok=False→ check the manifesterrorsfor the failed step (typically clean_smiles or save_features).could not convert string to float: '<value>'from save_features or main.py predict → input CSV has a non-numeric passthrough column (e.g. a 'split' label). The prep step now strips the CSV to SMILES-only at inference; if this error still surfaces, the CSV is being read by a runner that bypassed prepare_data. Re-run via the skill, notmain.pydirectly.--gpus '0,1' is single-GPU only→ pass a single id.
Replayability
The run.json cmd_replay field is a single-line command that re-runs the
inference with the same inputs. To replay inside the kermt container:
$(jq -r .cmd_replay $RUN_DIR/run.json)If ok_to_replay: false (dirty kermt repo worktree at launch time), pin
the commit via repo.commit and git checkout it first.
Files
- ATTRIBUTION.mdAdded by AutoGineer829 B
- BENCHMARK.md7.4 KiB
- LICENSELicence file added by AutoGineer11.2 KiB
- SKILL.md5.9 KiB
- config/defaults_inference.json528 B
- evals/evals.json5.5 KiB
- scripts/_utils.pyScript14.1 KiB
- scripts/check_checkpoint.pyScript20.1 KiB
- scripts/check_data.pyScript11.9 KiB
- scripts/kermt_container.shScript18.9 KiB
- scripts/prepare_data.pyScript36.5 KiB
- scripts/run_inference.pyScript10.5 KiB
- skill-card.md4.4 KiB
- skill.oms.sig6.1 KiB
scripts/check_data.py
#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Validate a CSV input for a given KERMT agent workflow.
Mode-dispatched. Emits a single JSON object to stdout that the calling skill
parses to decide whether to proceed (`ok: true`) or surface a structured error
back to the user (`ok: false` with `errors[]`). The JSON shape is stable
across modes; only the contract for what counts as `ok` differs per mode.
Modes
-----
pretrain Pretrain corpus CSV. Requires a `smiles` column. Other columns
are ignored. Label columns are not required (and not expected).
finetune Labeled CSV for a downstream task. Requires `smiles` plus
>=1 numeric target column. Target columns are specified via
`--targets <col1> <col2> ...`. If `--targets` is omitted, the
validator auto-detects numeric non-smiles columns and reports
them; the skill will then prompt the user to confirm or refine.
inference CSV to run predictions on. Requires `smiles`. Target columns are
not required (and not expected — predictions are written out).
embed CSV to extract embeddings from. Requires `smiles` only.
SMILES validation
-----------------
By default the validator samples up to 20 SMILES (first 10 + last 10) and
checks each one parses with RDKit. Pass `--strict-rdkit` to parse every
SMILES (slow on large corpora). A SMILES is considered "invalid" if RDKit
returns `None` from `MolFromSmiles(smi, sanitize=True)` — empty / null
rows are counted separately.
Duplicate-SMILES detection is always full (cheap).
Output (stdout)
---------------
{
"ok": true | false,
"mode": str,
"csv_path": str,
"num_rows": int,
"num_columns": int,
"columns": [str, ...],
"has_smiles_column": bool,
"smiles_column_name": str | null, // actual header used (may differ in case)
"num_blank_smiles": int,
"num_invalid_smiles": int, // among the parsed sample
"smiles_check_method": "sampled" | "full",
"smiles_check_count": int,
"num_duplicate_smiles": int,
"target_columns": [str, ...], // populated only for finetune mode
"num_missing_per_target": { col: int, ... },
"auto_detected_targets": [str, ...], // when --targets is omitted in finetune mode
"errors": [str, ...],
"warnings": [str, ...]
}
Exit code: 0 on `ok: true`, 1 on `ok: false`. Loader exceptions are caught
and surfaced into `errors[]` with `ok: false` (still exit 1).
CLI
---
check_data.py --mode <mode> --csv <path>
[--targets <col1> <col2> ...] # finetune only
[--strict-rdkit] # full SMILES parse
"""
from __future__ import annotations
import argparse
import json
import sys
import traceback
from pathlib import Path
from typing import Any
import pandas as pd
CANONICAL_SMILES_COLUMN = "smiles"
SMILES_SAMPLE_PER_END = 10 # how many SMILES from head + how many from tail to sample
def _find_smiles_column(columns: list[str]) -> str | None:
"""Return the actual column header matching 'smiles' case-insensitively, or None."""
for c in columns:
if c.lower() == CANONICAL_SMILES_COLUMN:
return c
return None
def _parse_smiles_sample(smiles_values: list[str], full: bool) -> tuple[int, int, str]:
"""Run RDKit MolFromSmiles on a sample or all of the SMILES. Returns
(num_parsed, num_invalid, method)."""
# Import here so the script can still surface a clean JSON error if RDKit
# is unavailable in the host env.
try:
from rdkit import Chem
from rdkit import RDLogger
RDLogger.DisableLog("rdApp.*") # suppress per-mol parse warnings
except ImportError as exc:
raise RuntimeError(
f"RDKit is not importable in this environment: {exc}. "
"Run check_data.py inside the kermt container."
) from exc
if full or len(smiles_values) <= 2 * SMILES_SAMPLE_PER_END:
sample = smiles_values
method = "full"
else:
sample = smiles_values[:SMILES_SAMPLE_PER_END] + smiles_values[-SMILES_SAMPLE_PER_END:]
method = "sampled"
invalid = 0
parsed = 0
for smi in sample:
if not smi: # already counted as blank elsewhere
continue
parsed += 1
mol = Chem.MolFromSmiles(smi, sanitize=True)
if mol is None:
invalid += 1
return parsed, invalid, method
def _autodetect_target_columns(df: pd.DataFrame, smiles_col: str) -> list[str]:
"""Pick columns that look like numeric targets. A column qualifies if it
is (a) not the smiles column and (b) >=80% of non-null values convert to float.
Heuristic only — returned for the skill to prompt the user to confirm."""
candidates: list[str] = []
for col in df.columns:
if col == smiles_col:
continue
ser = df[col].dropna()
if len(ser) == 0:
continue
try:
converted = pd.to_numeric(ser, errors="coerce")
except (TypeError, ValueError):
continue
if converted.notna().sum() / max(len(ser), 1) >= 0.8:
candidates.append(col)
return candidates
def validate(mode: str, csv_path: str, targets: list[str] | None, strict_rdkit: bool) -> dict[str, Any]:
result: dict[str, Any] = {
"ok": False,
"mode": mode,
"csv_path": csv_path,
"num_rows": 0,
"num_columns": 0,
"columns": [],
"has_smiles_column": False,
"smiles_column_name": None,
"num_blank_smiles": 0,
"num_invalid_smiles": 0,
"smiles_check_method": "sampled",
"smiles_check_count": 0,
"num_duplicate_smiles": 0,
"target_columns": [],
"num_missing_per_target": {},
"auto_detected_targets": [],
"errors": [],
"warnings": [],
}
# 1. Read the CSV.
path = Path(csv_path)
if not path.is_file():
result["errors"].append(f"CSV not found: {csv_path}")
return result
try:
df = pd.read_csv(path)
except pd.errors.EmptyDataError:
result["errors"].append(f"CSV is empty (no header): {csv_path}")
return result
except Exception as exc: # noqa: BLE001
result["errors"].append(f"failed to read CSV {csv_path}: {type(exc).__name__}: {exc}")
return result
result["num_rows"] = int(len(df))
result["num_columns"] = int(len(df.columns))
result["columns"] = [str(c) for c in df.columns]
# 2. Locate the SMILES column.
smiles_col = _find_smiles_column(result["columns"])
if smiles_col is None:
result["errors"].append(
f"no column named 'smiles' (case-insensitive) found in CSV. "
f"Available columns: {result['columns']}"
)
return result
result["has_smiles_column"] = True
result["smiles_column_name"] = smiles_col
if smiles_col != CANONICAL_SMILES_COLUMN:
result["warnings"].append(
f"SMILES column is named '{smiles_col}' but downstream code expects '{CANONICAL_SMILES_COLUMN}' "
f"(lowercase). Rename the column to '{CANONICAL_SMILES_COLUMN}' before running the workflow."
)
# 3. Blank-SMILES count + duplicate count + RDKit parse check.
smi_series = df[smiles_col].astype(str).fillna("").str.strip()
blank_mask = smi_series.eq("") | smi_series.str.lower().eq("nan")
result["num_blank_smiles"] = int(blank_mask.sum())
nonblank = smi_series[~blank_mask]
result["num_duplicate_smiles"] = int(len(nonblank) - nonblank.nunique())
if len(nonblank) == 0:
result["errors"].append("no non-blank SMILES found in the CSV")
return result
try:
parsed, invalid, method = _parse_smiles_sample(nonblank.tolist(), full=strict_rdkit)
except RuntimeError as exc:
result["errors"].append(str(exc))
return result
result["smiles_check_count"] = parsed
result["num_invalid_smiles"] = invalid
result["smiles_check_method"] = method
if invalid > 0:
scope = "all rows" if method == "full" else f"the {parsed} sampled rows"
result["errors"].append(
f"{invalid} out of {parsed} SMILES in {scope} failed to parse with RDKit. "
"Either pre-clean the CSV with scripts/clean_smiles.py or pass --strict-rdkit to see "
"the full count."
)
# 4. Target-column handling — finetune mode only.
if mode == "finetune":
if targets:
missing = [t for t in targets if t not in df.columns]
if missing:
result["errors"].append(
f"target column(s) not found in CSV: {missing}. "
f"Available columns: {result['columns']}"
)
else:
result["target_columns"] = list(targets)
for t in targets:
nan_count = int(df[t].isna().sum())
result["num_missing_per_target"][t] = nan_count
# Confirm numeric-ish.
nonnan = df[t].dropna()
converted = pd.to_numeric(nonnan, errors="coerce")
non_numeric_count = int(converted.isna().sum())
if non_numeric_count > 0:
result["warnings"].append(
f"target column '{t}' has {non_numeric_count} non-numeric value(s) "
f"that will be dropped by the finetune runner."
)
else:
# Auto-detect — surface candidates so the skill can prompt the user.
result["auto_detected_targets"] = _autodetect_target_columns(df, smiles_col)
if not result["auto_detected_targets"]:
result["errors"].append(
"no numeric non-smiles columns detected. finetune needs at least one target column; "
"specify it explicitly via --targets <col>."
)
else:
result["warnings"].append(
f"--targets was not specified; auto-detected candidate target columns "
f"{result['auto_detected_targets']}. The skill will prompt the user to confirm."
)
# 5. Small-corpus warning — only for pretrain (other modes can be tiny by design).
if mode == "pretrain" and result["num_rows"] < 100:
result["warnings"].append(
f"pretrain corpus is only {result['num_rows']} molecule(s). Pretraining typically "
f"needs orders of magnitude more — verify this is the intended input."
)
result["ok"] = not result["errors"]
return result
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Validate a CSV input for a KERMT agent workflow.")
parser.add_argument("--mode", required=True, choices=["pretrain", "finetune", "inference", "embed"])
parser.add_argument("--csv", required=True, help="Path to the input CSV")
parser.add_argument("--targets", nargs="+", default=None,
help="(finetune only) target column names. If omitted, the validator auto-detects "
"numeric non-smiles columns and reports them as candidates.")
parser.add_argument("--strict-rdkit", action="store_true",
help="Parse every SMILES with RDKit rather than sampling (slow on large CSVs).")
args = parser.parse_args(argv)
try:
result = validate(args.mode, args.csv, args.targets, args.strict_rdkit)
except Exception as exc: # noqa: BLE001
print(traceback.format_exc(), file=sys.stderr)
print(json.dumps({
"ok": False,
"mode": args.mode,
"csv_path": args.csv,
"errors": [f"unhandled exception in validator: {type(exc).__name__}: {exc}"],
"warnings": [],
}, indent=2))
return 1
print(json.dumps(result, indent=2))
return 0 if result["ok"] else 1
if __name__ == "__main__":
sys.exit(main())
Requirements and compatibility
Written for: Claude Code, OpenAI Codex
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 docker, nvidia-container-toolkit, and a CUDA-capable NVIDIA GPU. Designed for Claude Code, Codex, and Nemotron.
Security
What our scanner noticed· Medium risk
- Large base64-encoded blobs
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- NVIDIA
- Publisher
- NVIDIA (Official)
- Source
- NVIDIA/skills · skills/bionemo-kermt-infer · 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.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download kermt-infer.zip