Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .agents/skills/uv-build/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,19 @@ mise run bootstrap-nss cpu # + engine + CPU PyTorch
mise run bootstrap-nss cu129 # + engine + CUDA 12.9 PyTorch
mise run bootstrap-nss cuda # alias for cu129
mise run bootstrap-nss engine # + engine (no torch)

# Slurm: force Python, caches, and the project venv onto Lustre
LUSTRE_DIR="/path/to/container-visible/project/directory" \
Comment thread
zywind marked this conversation as resolved.
MISE_IGNORED_CONFIG_PATHS="$HOME/.config/mise/config.toml" \
MISE_LOCKED=1 mise run bootstrap-nss-slurm cu129
```

Under the hood: `uv sync --frozen --extra <extra> [--extra engine] --group dev`

`bootstrap-nss-slurm` requires `LUSTRE_DIR`, installs the pinned Python under
that directory, recreates `.venv` if its interpreter is not container-visible,
then runs the same frozen profile sync as `bootstrap-nss`.

## Extras and Conflicts

| Extra | What it installs |
Expand Down
24 changes: 24 additions & 0 deletions .mise/tasks/_lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,30 @@ resolve_venv_path() {
printf '%s\n' "${venv_path}"
}

sync_nss_dependencies() {
local extra="${1:?Python dependency profile is required}"
local sync_uv_bin="${NSS_UV_BIN:-uv}"

case "${extra}" in
cuda|cu129)
"${sync_uv_bin}" sync --frozen --extra cu129 --extra engine --group dev
;;
cpu)
"${sync_uv_bin}" sync --frozen --extra cpu --extra engine --group dev
;;
engine)
"${sync_uv_bin}" sync --frozen --extra engine --group dev
;;
dev)
"${sync_uv_bin}" sync --frozen --group dev
;;
*)
echo "Error: Invalid extra '${extra}'. Use one of: dev engine cpu cuda cu129" >&2
return 1
;;
esac
}

resolve_container_cmd() {
local container_cmd="${CONTAINER_CMD:-$(command -v podman 2>/dev/null || command -v docker 2>/dev/null || true)}"

Expand Down
21 changes: 2 additions & 19 deletions .mise/tasks/bootstrap-nss
Original file line number Diff line number Diff line change
Expand Up @@ -7,27 +7,10 @@
#USAGE }

set -euo pipefail
source "${MISE_CONFIG_ROOT}/.mise/tasks/_lib.sh"

mise run venv

echo "~~~~~~"
echo "attempting to install nss package with primary extra: ${usage_extra?}"

case "${usage_extra?}" in
cuda|cu129)
uv sync --frozen --extra cu129 --extra engine --group dev
;;
cpu)
uv sync --frozen --extra cpu --extra engine --group dev
;;
engine)
uv sync --frozen --extra engine --group dev
;;
dev)
uv sync --frozen --group dev
;;
*)
echo "Error: Invalid extra '${usage_extra?}'. Use one of: dev engine cpu cuda cu129" >&2
exit 1
;;
esac
sync_nss_dependencies "${usage_extra?}"
99 changes: 99 additions & 0 deletions .mise/tasks/bootstrap-nss-slurm
Comment thread
kendrickb-nvidia marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#MISE description="Install project dependencies with Python, caches, and the virtualenv on Lustre for Slurm containers."
#USAGE arg "<extra>" help="Python dependency profile" {
#USAGE choices "dev" "engine" "cpu" "cuda" "cu129"
#USAGE }

set -euo pipefail
source "${MISE_CONFIG_ROOT}/.mise/tasks/_lib.sh"

user_name="${NSS_SLURM_USER:-${USER_NAME:-$(id -un)}}"
lustre_dir="${LUSTRE_DIR:-}"
if [[ -z "${lustre_dir}" ]]; then
echo "Error: LUSTRE_DIR must be set to the user's container-visible project directory" >&2
exit 1
fi

python_version="${NSS_PYTHON_VERSION:-$(resolve_python_version)}"
venv_path="$(resolve_venv_path)"
uv_bin="$(mise which uv)"
readonly user_name lustre_dir python_version venv_path uv_bin

if [[ ! -d "${lustre_dir}" ]]; then
echo "Error: LUSTRE_DIR does not exist: ${lustre_dir}" >&2
exit 1
fi

lustre_real="$(readlink -f "${lustre_dir}")"
venv_parent_real="$(readlink -f "$(dirname "${venv_path}")")"
readonly lustre_real venv_parent_real
if [[ "${venv_parent_real}" != "${lustre_real}" && "${venv_parent_real}" != "${lustre_real}/"* ]]; then
echo "Error: Slurm virtualenv must be located under Lustre: ${venv_path}" >&2
exit 1
fi

export USER_NAME="${user_name}"
export LUSTRE_DIR="${lustre_dir}"
export NSS_DIR="${MISE_CONFIG_ROOT}"
export UV_CACHE_DIR="${LUSTRE_DIR}/.cache/uv"
export UV_PYTHON_INSTALL_DIR="${LUSTRE_DIR}/.local/share/uv/python"
export UV_PYTHON_BIN_DIR="${LUSTRE_DIR}/.local/bin"
export UV_TOOL_DIR="${LUSTRE_DIR}/.local/share/uv/tools"
export HF_HOME="${LUSTRE_DIR}/.cache/huggingface"
export VLLM_CACHE_ROOT="${LUSTRE_DIR}/.cache/vllm"
export NSS_UV_BIN="${uv_bin}"

mkdir -p \
"${UV_CACHE_DIR}" \
"${UV_PYTHON_INSTALL_DIR}" \
"${UV_PYTHON_BIN_DIR}" \
"${UV_TOOL_DIR}" \
"${HF_HOME}" \
"${VLLM_CACHE_ROOT}"

echo "[slurm-bootstrap] installing Python ${python_version} under ${UV_PYTHON_INSTALL_DIR}"
"${uv_bin}" python install --no-bin --install-dir "${UV_PYTHON_INSTALL_DIR}" "${python_version}"

python_bin="$(PATH=/usr/bin:/bin VIRTUAL_ENV= \
"${uv_bin}" python find --managed-python --no-project "${python_version}")"
readonly python_bin
if [[ ! -x "${python_bin}" ]]; then
echo "Error: uv did not resolve a managed Lustre Python executable: ${python_bin}" >&2
exit 1
fi

expected_python="$(readlink -f "${python_bin}")"
readonly expected_python
if [[ "${expected_python}" != "${lustre_real}/"* ]]; then
echo "Error: managed Python is not container-visible under Lustre: ${expected_python}" >&2
exit 1
fi

if [[ -e "${venv_path}" ]]; then
current_python="$(readlink -f "${venv_path}/bin/python" 2>/dev/null || true)"
if [[ "${current_python}" != "${expected_python}" ]]; then
echo "[slurm-bootstrap] recreating ${venv_path}; current Python is ${current_python:-unavailable}"
rm -rf "${venv_path}"
fi
fi

"${uv_bin}" venv --seed --allow-existing --python "${python_bin}" "${venv_path}"

echo "[slurm-bootstrap] syncing NSS dependencies with profile ${usage_extra?}"
sync_nss_dependencies "${usage_extra?}"
Comment thread
zywind marked this conversation as resolved.

venv_python="$(readlink -f "${venv_path}/bin/python")"
readonly venv_python
if [[ "${venv_python}" != "${lustre_real}/"* ]]; then
echo "Error: virtualenv Python is not container-visible under Lustre: ${venv_python}" >&2
exit 1
fi

if ! "${venv_path}/bin/safe-synthesizer" --help >/dev/null 2>&1; then
echo "Error: Safe Synthesizer entry point is not usable in ${venv_path}" >&2
exit 1
fi

echo "[slurm-bootstrap] ready: ${venv_python}"
115 changes: 73 additions & 42 deletions script/slurm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ Jobs are submitted via `submit_slurm_jobs.sh`, which launches a containerized `s
- `submit_slurm_jobs.sh`: Submits Slurm array jobs for each config and dataset. Supports two-stage TRAIN→GEN pipeline.
- `slurm_nss_matrix.sh`: Picks dataset and config and launches the python entrypoint inside the container. Honors `NSS_PHASE=train|generate|end_to_end`.
- `slurm_srun.sh`: Wraps `srun` with container image and mounts, mostly just a pass through, primary logic is in `submit_slurm_jobs.sh` and `slurm_nss_matrix.sh`.
- `.mise/tasks/bootstrap-nss-slurm`: Installs a container-visible Python and project virtualenv under the current user's Lustre directory.
- `configs/*.yaml`: Major configs we support. Use the config basenames from this directory in commands (for example, `smollm3-nodp`, `smollm3-dp`, etc.). The current set is the cross product of 3 pre-trained models and 2 DP settings (on or off).

Pipeline entrypoints (invoked by Slurm scripts) via uv:
- `uv run safe-synthesizer run --run-path <path>` (full end-to-end pipeline)
- `uv run safe-synthesizer run train --run-path <path>` (PII replacement + training only)
- `uv run safe-synthesizer run generate --run-path <path>` (generation + evaluation only)
Pipeline entrypoints invoked from the prebuilt project virtualenv:
- `.venv/bin/safe-synthesizer run --run-path <path>` (full end-to-end pipeline)
- `.venv/bin/safe-synthesizer run train --run-path <path>` (PII replacement + training only)
- `.venv/bin/safe-synthesizer run generate --run-path <path>` (generation + evaluation only)

### Prerequisites

Expand All @@ -27,59 +28,88 @@ Pipeline entrypoints (invoked by Slurm scripts) via uv:
- Weights & Biases API Key: W&B logging is enabled by default (`WANDB_MODE=online`). You will need a `WANDB_API_KEY` — request an account [here](https://confluence.nvidia.com/display/AIALGO/Weights+and+Biases+%28WandB%29+Enterprise+Account). Set `WANDB_MODE=disabled` in `env_variables.sh` to skip W&B.
- Enroot Credentials: Follow https://confluence.nvidia.com/display/HWINFCSSUP/Using+Containers#UsingContainers-SettingupEnrootCredentials. You should add the lines for all 3 of `nvcr.io`, `authn.nvidia.com`, and `gitlab-master.nvidia.com`.
- Clone Safe-Synthesizer

The instructions below assume that Safe-Synthesizer is cloned directly under
`LUSTRE_DIR` and that commands after cloning are run from the repository root.

```bash
export USER_NAME="$USER" # Or hardcode username in slurm
export LUSTRE_DIR="/lustre/fsw/portfolios/nemotron/projects/nemotron_data_dev/users/${USER_NAME}"
cd $LUSTRE_DIR
cd "${LUSTRE_DIR}"
git clone git@github.com:NVIDIA-NeMo/Safe-Synthesizer.git
cd Safe-Synthesizer
```
- uv and python install in the slurm cluster
- DO NOT FOLLOW the general CONTRIBUTING.md or README.md instructions for installation and setup, unless you understand exactly what's being installed where and how that interacts with the distributed nature of a slurm cluster.
- The following setup is strongly recommended, but is not the only way to get things working.
- The key issues about working in slurm we need to address
- /home/$USER is quite small (10 GB) and not recommended for accessing data (easily filled up by uv cache)
- Slurm jobs may run in containers with different $HOME locations (and different users/uids)
- Thus we put uv and python in your user directory in /lustre and not in /home/$USER

#### Bootstrap the Slurm Python environment

Do not use the general development bootstrap for Slurm. Slurm containers mount
`/lustre`, but may not have access to the login node's `/home` directory. The
Python interpreter, project virtualenv, package caches, and model caches must
therefore resolve under Lustre.

From the repository root, install the repository-pinned mise tools and run the
Comment thread
binaryaaron marked this conversation as resolved.
Slurm-specific bootstrap:

```bash
export USER_NAME="$USER" # Or hardcode username in slurm
export LUSTRE_DIR="/lustre/fsw/portfolios/nemotron/projects/nemotron_data_dev/users/${USER_NAME}"
# Install `uv` to your lustre directory
curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="$LUSTRE_DIR/.uv/bin" sh
# Set environment variables so `uv` uses $LUSTRE_DIR subdirectories for storage
export UV_CACHE_DIR="${LUSTRE_DIR}/.cache/uv"
export UV_PYTHON_INSTALL_DIR="${LUSTRE_DIR}/.local/share/uv/python"
export UV_PYTHON_BIN_DIR="${LUSTRE_DIR}/.local/bin"
export UV_TOOL_DIR="${LUSTRE_DIR}/.local/share/uv/tools"
# With the above env vars, the usual mise task should work.
# Note this may be quite slow the first time due to very slow network
# connectivity on slurm to download from pypi, but subsequent executions
# (such as startup for your jobs) should be much faster since uv will
# pull cached wheels from UV_CACHE_DIR.
# (Be sure to run from the root of the Safe-Synthesizer repo)
mise run bootstrap-nss cu129
make install-mise
export PATH="${HOME}/.local/bin:${PATH}"
export MISE_IGNORED_CONFIG_PATHS="${HOME}/.config/mise/config.toml"
mise install --locked
MISE_LOCKED=1 mise run bootstrap-nss-slurm cu129
```

`cu129` and `cuda` select the same CUDA 12.9 dependency profile.
Existing Slurm checkouts should run this bootstrap once after pulling the
change. Scheduled launch environments, including GitLab jobs, must also add
`${HOME}/.local/bin` to `PATH` before invoking `submit_slurm_jobs.sh`.

#### Nice to have
The task requires the exported `LUSTRE_DIR`, derives the Slurm username from
`id -un`, uses mise's pinned `uv`, installs the pinned Python under that Lustre
directory, and creates `.venv` with that exact interpreter. If an existing
`.venv` points outside Lustre, the task recreates it. Existing Python and uv
installations under `/home` do not need to be removed.

Override the inferred user or select a different Lustre project directory when
the cluster account layout requires it:

- Passwordless login See https://confluence.nvidia.com/display/HWINFCSSUP/Setting+Up+Passwordless+SSH+Key+Authentication?src=contextnavpagetreemode
- Env vars in .bashrc
- Add `export VARIABLE=VALUE` to the end of `~/.bashrc` for commonly used environment variables, like `USER_NAME` and `LUSTRE_DIR`.
- Recommended snippet to have in `~/.bashrc` so uv and python work on login node and slurm jobs:
```bash
export USER_NAME=<your slurm user name>
NSS_SLURM_USER="your_user" MISE_LOCKED=1 mise run bootstrap-nss-slurm cu129
LUSTRE_DIR="/custom/lustre/path" MISE_LOCKED=1 mise run bootstrap-nss-slurm cu129
```

Verify the postcondition before submitting jobs:

```bash
readlink -f .venv/bin/python
.venv/bin/safe-synthesizer --help >/dev/null
Comment thread
binaryaaron marked this conversation as resolved.
```

The Python path must resolve beneath the canonical path reported by
`readlink -f "${LUSTRE_DIR}"`. `cu129` and `cuda` select the same CUDA 12.9
dependency profile.

Repo mode, the default when `--nss-version` is omitted, does not require a
separate uv installation under Lustre. PyPI mode uses uv inside the container
and still requires `${LUSTRE_DIR}/.uv/bin/env`.

For PyPI mode only, install the same uv version that mise has pinned:

```bash
export USER_NAME="${USER_NAME:-$(id -un)}"
export LUSTRE_DIR="/lustre/fsw/portfolios/nemotron/projects/nemotron_data_dev/users/${USER_NAME}"
uv_version="$(mise exec -- uv --version | awk '{print $2}')"
curl -LsSf "https://astral.sh/uv/${uv_version}/install.sh" \
| env UV_INSTALL_DIR="${LUSTRE_DIR}/.uv/bin" sh
```

#### Nice to have

# (May be added automatically by uv)
. "${LUSTRE_DIR}/.uv/bin/env"
- Passwordless login See https://confluence.nvidia.com/display/HWINFCSSUP/Setting+Up+Passwordless+SSH+Key+Authentication?src=contextnavpagetreemode
- Env vars in `.bashrc`
- This is optional for bootstrap, but avoids exporting the submission user in every shell.

export UV_CACHE_DIR="${LUSTRE_DIR}/.cache/uv"
export UV_PYTHON_INSTALL_DIR="${LUSTRE_DIR}/.local/share/uv/python"
export UV_PYTHON_BIN_DIR="${LUSTRE_DIR}/.local/bin"
export UV_TOOL_DIR="${LUSTRE_DIR}/.local/share/uv/tools"
export HF_HOME="${LUSTRE_DIR}/.cache/huggingface"
```bash
export USER_NAME="<your slurm user name>"
export LUSTRE_DIR="/lustre/fsw/portfolios/nemotron/projects/nemotron_data_dev/users/${USER_NAME}"
```


Expand Down Expand Up @@ -255,6 +285,7 @@ W&B is enabled by default with `WANDB_MODE=online` in `env_variables.sh`. Make s
- Missing token file/key: create `${LUSTRE_DIR}/.api_tokens.sh` with `NSS_INFERENCE_KEY` and `chmod 600`.
- Missing config files: verify `CONFIGS` in `env_variables.sh` and files in `CONFIG_DIR`.
- Permission errors: confirm your `/lustre/.../${USER_NAME}` paths and file perms.
- Virtualenv Python under `/home`: run `MISE_LOCKED=1 mise run bootstrap-nss-slurm cu129`; the task recreates `.venv` with a Lustre-managed interpreter.

#### cpu bind errors

Expand Down
18 changes: 13 additions & 5 deletions script/slurm/slurm_nss_matrix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ else
config=${all_configs[0]}
fi

# Fail before installing system packages when PyPI mode cannot access uv.
if [[ -n "${NSS_VERSION:-}" && ! -f "${LUSTRE_DIR}/.uv/bin/env" ]]; then
echo "[NSS SLURM] ERROR: PyPI mode requires uv at ${LUSTRE_DIR}/.uv/bin" >&2
echo "[NSS SLURM] Install uv as described in script/slurm/README.md," \
"or omit --nss-version to use repo mode." >&2
exit 1
fi

# Ensure minimal build toolchain inside container (no-op if already present)
apt-get update && apt-get install -y --no-install-recommends \
curl \
Expand All @@ -104,15 +112,13 @@ apt-get update && apt-get install -y --no-install-recommends \
libcurl4 \
libcurl3-gnutls

# Ensure Python environment is available inside the container
source "${LUSTRE_DIR}/.uv/bin/env"

# NSS_PYTHON_VERSION is resolved in env_variables.sh (from .python-version, with
# an optional override), which is always sourced before this script runs.

if [[ -n "${NSS_VERSION:-}" ]]; then
# Install nemo-safe-synthesizer from PyPI into a versioned venv cached on
# lustre so concurrent array jobs can share it without redundant downloads.
source "${LUSTRE_DIR}/.uv/bin/env"
PYPI_VENV="${LUSTRE_DIR}/.venv_nss_py${NSS_PYTHON_VERSION}_${NSS_VERSION}"
uv venv --python "${NSS_PYTHON_VERSION}" "${PYPI_VENV}"
source "${PYPI_VENV}/bin/activate"
Expand All @@ -128,8 +134,10 @@ else
# submit_slurm_jobs.sh, rather than each array task running `uv sync` against
# the shared Lustre .venv (which could race and corrupt it).
if [[ ! -x "${NSS_DIR}/.venv/bin/safe-synthesizer" ]]; then
echo "[NSS SLURM] ERROR: no usable repo venv at ${NSS_DIR}/.venv (missing safe-synthesizer entry point)" >&2
echo "[NSS SLURM] Submit via submit_slurm_jobs.sh (it builds the venv once), or build it manually: (cd ${NSS_DIR} && mise run bootstrap-nss cu129)" >&2
echo "[NSS SLURM] ERROR: no usable repo venv at ${NSS_DIR}/.venv" \
"(missing safe-synthesizer entry point)" >&2
echo "[NSS SLURM] Submit via submit_slurm_jobs.sh (it builds the venv once)," \
"or build it manually: (cd ${NSS_DIR} && mise run bootstrap-nss-slurm cu129)" >&2
exit 1
fi
echo "[NSS SLURM] Using pre-built repo venv at ${NSS_DIR}/.venv"
Expand Down
Loading
Loading