diff --git a/README.md b/README.md index d49f60f53..49f2e8a18 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,15 @@ NVIDIA NeMo Safe Synthesizer creates private, safe versions of sensitive tabular ## Quick Start + + + + Launch on Brev + + + +Try it without installing anything: the launchable above deploys a GPU instance with NeMo Safe Synthesizer and the tutorial notebooks preinstalled. Note that the instance bills continuously and cannot be paused -- delete it when you are finished. + Read detailed usage below, or jump to the documentation with [Getting Started](https://nvidia-nemo.github.io/Safe-Synthesizer/user-guide/getting-started/) or the [Safe Synthesizer 101](https://nvidia-nemo.github.io/Safe-Synthesizer/tutorials/safe-synthesizer-101/) notebook. diff --git a/docs/index.md b/docs/index.md index cc0b36f1a..6880c771d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -17,6 +17,21 @@ NeMo Safe Synthesizer creates private, safe versions of sensitive tabular datase - Comprehensive evaluation -- Privacy and quality metrics in an in-depth HTML report - Flexible interfaces -- CLI for scripting, Python SDK for programmatic workflows, YAML configuration +## Try It Without Installing + +[![Launch on Brev](https://brev-assets.s3.us-west-1.amazonaws.com/nv-lb-dark.svg#only-light)](https://brev.nvidia.com/launchable/deploy/now?launchableID=env-3HBtA2NKQaBukL2TyDphWUcvQ17) +[![Launch on Brev](https://brev-assets.s3.us-west-1.amazonaws.com/nv-lb-light.svg#only-dark)](https://brev.nvidia.com/launchable/deploy/now?launchableID=env-3HBtA2NKQaBukL2TyDphWUcvQ17) + +Deploys an NVIDIA GPU instance with NeMo Safe Synthesizer and the tutorial notebooks +already installed, so there is no CUDA, driver, or Python setup to do. The +[Safe Synthesizer 101](tutorials/safe-synthesizer-101.ipynb) notebook runs end to end in +about 15 minutes. + +!!! warning "The instance bills continuously" + Most GPU providers on Brev do not support stopping an instance. Billing runs from + creation until deletion, so delete the instance when you are finished and download + anything you want to keep first. + !!! info "System Requirements" NeMo Safe Synthesizer requires a Linux machine with an NVIDIA GPU (A100 80GB+ recommended) and CUDA 12.9+ to run the training and generation pipeline. macOS, Windows, and Apple Silicon are not supported for pipeline execution. A CPU-only install is available for development and configuration validation -- see [Getting Started](user-guide/getting-started.md#install-the-package). diff --git a/docs/tutorials/index.md b/docs/tutorials/index.md index 9d481e756..126354691 100644 --- a/docs/tutorials/index.md +++ b/docs/tutorials/index.md @@ -5,6 +5,17 @@ Interactive Jupyter notebook tutorials for NeMo Safe Synthesizer. +[![Launch on Brev](https://brev-assets.s3.us-west-1.amazonaws.com/nv-lb-dark.svg#only-light)](https://brev.nvidia.com/launchable/deploy/now?launchableID=env-3HBtA2NKQaBukL2TyDphWUcvQ17) +[![Launch on Brev](https://brev-assets.s3.us-west-1.amazonaws.com/nv-lb-light.svg#only-dark)](https://brev.nvidia.com/launchable/deploy/now?launchableID=env-3HBtA2NKQaBukL2TyDphWUcvQ17) + +These notebooks need a Linux machine with an NVIDIA GPU. The launchable above provides +one with Safe Synthesizer and all three notebooks already installed. + +!!! warning "The instance bills continuously" + Most GPU providers on Brev do not support stopping an instance. Billing runs from + creation until deletion, so delete the instance when you are finished and download + anything you want to keep first. + ## Available Tutorials - [Safe Synthesizer 101](safe-synthesizer-101.ipynb) -- learn the fundamentals diff --git a/docs/user-guide/getting-started.md b/docs/user-guide/getting-started.md index 7fc4aea49..cf900d36b 100644 --- a/docs/user-guide/getting-started.md +++ b/docs/user-guide/getting-started.md @@ -26,6 +26,14 @@ does at each stage. ### Install the Package +!!! tip "Skip installation entirely" + [![Launch on Brev](https://brev-assets.s3.us-west-1.amazonaws.com/nv-lb-dark.svg#only-light)](https://brev.nvidia.com/launchable/deploy/now?launchableID=env-3HBtA2NKQaBukL2TyDphWUcvQ17) + [![Launch on Brev](https://brev-assets.s3.us-west-1.amazonaws.com/nv-lb-light.svg#only-dark)](https://brev.nvidia.com/launchable/deploy/now?launchableID=env-3HBtA2NKQaBukL2TyDphWUcvQ17) + + Deploys a GPU instance with everything below already done, plus the tutorial + notebooks. Useful for evaluating Safe Synthesizer without a local NVIDIA GPU. The + instance bills continuously and cannot be paused -- delete it when you are finished. + The CUDA and CPU extras depend on packages (PyTorch, FlashInfer) hosted on indexes outside PyPI. You must pass the extra index URLs shown below. diff --git a/script/brev/README.md b/script/brev/README.md new file mode 100644 index 000000000..084df2f88 --- /dev/null +++ b/script/brev/README.md @@ -0,0 +1,190 @@ + + + +### NeMo Safe Synthesizer Brev Launchable + +This directory holds the provisioning script and recorded console settings for the +[NVIDIA Brev](https://brev.nvidia.com) Launchable, a one-click GPU environment for trying +NeMo Safe Synthesizer without setting up CUDA, drivers, or Python locally. + +Nothing in this directory is executed by the repo or by CI. A Brev Launchable is +configured in the Brev web console, and the setup script is pasted into a form field +there. This directory exists so that configuration is versioned and reviewable rather +than living only in a browser. When you change `setup.sh`, you must also paste the new +contents into the console for the change to take effect. + +### Files + +- `setup.sh`: Pasted into the Launchable's Setup Script field. Installs the CUDA + build of Safe Synthesizer into a dedicated venv, registers it as the default Jupyter + kernel, and drops the tutorial notebooks in `$HOME`. +- `welcome.md`: Becomes the customer's `$HOME/README.md`. Fetched at provisioning + time from the same tarball as the tutorials, not baked into `setup.sh` -- it would + otherwise consume a tenth of the 16 KiB script budget. + +### Console configuration + +Recreate the Launchable at → Launchables → Create +Launchable with these settings. + +| Section | Field | Value | +| ------- | ----- | ----- | +| Software | Runtime mode | VM Mode | +| Software | Install Jupyter on the host | Enabled | +| Software | Run a Setup Script | Enabled, contents of `setup.sh` | +| Software | Image ID | Leave blank | +| Source | Code source | No code files (`setup.sh` downloads the tutorials itself) | +| Hardware | GPU | 1× 80 GiB VRAM, single GPU | +| Hardware | Disk | 200 GiB or more -- not resizable after creation | +| Network | Ports | 8888, named `jupyter` | +| Access | Visibility | Anyone with the link | + +Multi-GPU instances are pointless here: training is single-GPU only, so a second GPU +adds cost and no capability. + +### Launch parameters + +All optional. Per Brev's guidance, do not put credential values in parameter defaults. + +| Name | Type | Purpose | +| ---- | ---- | ------- | +| `NSS_INFERENCE_KEY` | Text | NVIDIA NIM key for column classification. Without it, classification runs in degraded mode. | +| `HF_TOKEN` | Text | Only needed for gated Hugging Face models. | + +Brev passes these to `setup.sh` as environment variables. + +Deliberately kept to two. A dataset-URL parameter and a model-prewarm toggle were both +tried and removed: a customer can fetch a URL with one `!curl` line in a notebook, and +prewarming moves the model download earlier without making it shorter, while hiding the +progress bar the notebook would otherwise show. Every parameter is visible to every +deployer, so the bar for adding one is high. + +### What the customer gets + +`$HOME` is the JupyterLab file browser root, so only what a customer needs is visible +there. Everything operational is a dotfile, which the browser hides by default. + +```text +$HOME/ + tutorials/ the three tutorial notebooks and their datasets + README.md where to start, rendered on double-click + (SETUP-IN-PROGRESS.md until setup finishes) + + .nss-venv/ cu129 venv, registered as the default kernel + .cache/huggingface/ model cache (Hugging Face's default location) + .nss-env.sh PATH and VIRTUAL_ENV, sourced from .bashrc + .nss-setup.log full provisioning log +``` + +The script deliberately does not create a data folder. Customers put their files +wherever they like and pass the path to `with_data_source()`. Bring-your-own-data +options, in rough order of convenience: drag and drop into the JupyterLab file browser; +`!curl -O ` in a notebook cell; or `brev shell` plus `scp` for large files. + +### Implementation notes + +These are the non-obvious constraints the script works around. They were each found the +hard way on a real instance. + +- Package indexes are derived at runtime, not hardcoded. Index URLs are + install-time configuration rather than wheel metadata, so the installer must supply + them -- and they have to match the release being installed, not this repo's `main`. + The script resolves the latest version from the PyPI JSON API, fetches that tag's + `pyproject.toml`, and reads the CUDA index URLs out of it, then pins the install to + that exact version so the two cannot drift. Selection is keyed on the URL containing + `cu129`, not on the index name: the flashinfer entry was renamed + `flashinfer-jit-cache` → `flashinfer-jit-cache-cu129` between 0.1.8 and 0.1.9, so + names are not stable across releases. The parse runs inside a process substitution and + therefore cannot fail the script, so the count of discovered indexes is what validates + it. +- uv is installed from a checksum-verified tarball, not `curl | sh`. The + `astral.sh/install.sh` path logs `no checksums to verify`, so nothing validated what + it downloaded. The script fetches the pinned release tarball, compares it against the + published `.sha256`, and installs the binaries -- matching the GPG-verified mise + install in `tools/install-mise.sh`. +- A placeholder file marks the home directory while setup runs. JupyterLab + accepts connections well before this script finishes, so a user who opens it early + would otherwise see an empty or half-populated file browser and assume the Launchable + is broken. `SETUP-IN-PROGRESS.md` is written before any slow work, rewritten by the + `ERR` trap if provisioning fails, and replaced by `README.md` on success. +- The welcome text lives in `welcome.md`, not a heredoc. It is pulled from the + same tarball as the tutorials, so the two always match, and it is staged as a dotfile + until the final step so it never appears while setup is still running. The fetch is + non-fatal: `script/brev/` exists in no released tag, so it resolves only from the + `main` fallback until a release includes it. +- The setup script has a 16 KiB limit. Brev rejects anything larger, which is why + the script carries short comments pointing here rather than full explanations. Check + `wc -c script/brev/setup.sh` before pasting. +- The script runs unprivileged. Brev executes it as the instance user, not root, so + `/usr/local/bin`, `/var/log`, and `/etc/profile.d` are all unwritable. Everything + installs under `$HOME`. +- The venv is deliberately separate from Brev's. Installing `[cu129,engine]` into + `$HOME/.venv` would resolve torch, vllm, and flashinfer alongside jupyterlab's own + pins. If that breaks the notebook server, the customer's only interface is gone and + there is no error they can act on. A separate venv keeps a failed install recoverable. +- The instance username varies by provider -- `shadeform` on the Shadeform-brokered + offerings, likely `ubuntu` on direct ones. The script never names an account; it uses + `$HOME` and `id -un`. +- The image ships its own `$HOME/.venv` on Python 3.12, which is what the Brev + Jupyter server runs in. `uv` discovers it by walking up from the working directory, so + the script pins `VIRTUAL_ENV` in both the kernelspec and `.nss-env.sh` to keep + `uv pip install` from silently targeting it. Do not delete or upgrade that venv -- + breaking it breaks the notebook server, which is the customer's only interface. +- The kernel is registered as `python3`, not a custom name, because the tutorial + notebooks declare `kernelspec.name = "python3"`. Any other name would force the + customer to switch kernels by hand. +- Kernel precedence is counterintuitive. `jupyter_core` puts the server's own + environment *ahead* of `~/.local/share/jupyter` whenever the server runs inside a + virtualenv (`prefer_environment_over_user()`), and Brev's does. Registering only at + user scope is silently shadowed by Brev's 3.12 kernel. The script asks the server's + interpreter for `jupyter_path("kernels")[0]` and writes there, backing up any existing + `kernel.json` to `kernel.json.orig`. +- The kernelspec holds secrets, so it stays mode 0600 inside `$HOME`. An earlier + version also mirrored it to `/usr/local/share/jupyter` at 0644 via sudo, which would + have put `NSS_INFERENCE_KEY` and `HF_TOKEN` in a world-readable file. Do not + reintroduce that -- it also could not win precedence over a venv-scoped kernel anyway. +- Most providers do not support stop/start. The instance bills from creation until + deletion, with no pause. Nebius was the exception at the time of writing. +- Tutorials arrive via tarball, not `git clone`. `--strip-components` lands them flat + in `tutorials/` rather than the repo's `docs/tutorials/` nesting, and the image is not + required to have `git`. The tarball is pinned to the tag matching the installed wheel, + falling back to `main`, so the notebooks can never call APIs newer than the package + they run against. +- The tar member is an exact path, not a glob. GNU tar does not enable wildcards for + member names by default, and bsdtar rejects `--wildcards` outright, so no single glob + form works on both. The archive's top-level directory name also varies by ref + (`Safe-Synthesizer-0.1.8` for a tag, `Safe-Synthesizer-main` for a branch), so the + script reads it from the archive rather than guessing. +- Never pipe `tar -t` into `head`. Closing the pipe early makes GNU tar fail with + `stdout: write error` on SIGPIPE, and `pipefail` turns that into a failed run. bsdtar + exits silently on SIGPIPE, so this passes on macOS and fails on the instance. The + script reads the listing into a variable and takes the prefix with `${listing%%/*}`. + +### Verification + +After deploying, check `$HOME/.nss-setup.log` for the `setup complete` banner, then in a +notebook confirm the environment resolved correctly: + +```python +import sys +print(sys.executable) # expect $HOME/.nss-venv/bin/python +``` + +That must hold without touching the kernel picker -- the customer should never have +to switch kernels. If it reports a 3.12 path instead, kernel registration landed in a +directory the server does not consult; check the `registered kernel at ...` lines in the +log against `jupyter kernelspec list`. + +Then run `tutorials/safe-synthesizer-101.ipynb` end to end -- roughly 15 minutes on +an A100 or H100. + +### Tested configurations + +| Date | Provider | GPU | Result | +| ---- | -------- | --- | ------ | +| 2026-07-29 | Shadeform-brokered | H100 PCIe 80 GiB | uv install, venv creation, `[cu129,engine]` install, tutorial fetch | + +Add a row when you validate another provider or GPU class. The A100-or-larger figure in +the main docs looks conservative for the default 3B model; if a 48 GiB card proves +sufficient, record it here and correct the requirement in `README.md`, +`docs/index.md`, and `docs/user-guide/getting-started.md`. diff --git a/script/brev/setup.sh b/script/brev/setup.sh new file mode 100755 index 000000000..a123e5cf7 --- /dev/null +++ b/script/brev/setup.sh @@ -0,0 +1,373 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# setup.sh -- provision a Brev VM-Mode Launchable for NeMo Safe Synthesizer. +# Paste into the Launchable's Setup Script field. Runs once, unprivileged, +# after Jupyter is installed; everything lands under $HOME. Idempotent. +# +# Launch parameters, passed by Brev as environment variables: +# NSS_INFERENCE_KEY NIM key for column classification (degraded without it) +# HF_TOKEN only for gated Hugging Face models +# +# script/brev/README.md explains every non-obvious step below. Each one exists +# because a deploy failed without it -- read it before simplifying anything. +set -euo pipefail + +readonly UV_VERSION="0.9.30" +readonly PYTHON_VERSION="3.13" +# No .git suffix: this is only used to build archive URLs, not to clone. +readonly REPO_URL="https://github.com/NVIDIA-NeMo/Safe-Synthesizer" + +: "${HOME:?HOME is not set}" + +# $HOME is the file browser root: only tutorials/ and README.md are visible. +readonly TUTORIALS_DIR="${HOME}/tutorials" +readonly README_FILE="${HOME}/README.md" +readonly WAIT_FILE="${HOME}/SETUP-IN-PROGRESS.md" +readonly WELCOME_STAGED="${HOME}/.nss-welcome.md" + +readonly BIN_DIR="${HOME}/.local/bin" +readonly VENV_DIR="${HOME}/.nss-venv" +readonly USER_KERNEL_DIR="${HOME}/.local/share/jupyter/kernels/python3" +readonly ENV_FILE="${HOME}/.nss-env.sh" +readonly LOG_FILE="${HOME}/.nss-setup.log" + +readonly CUDA_EXTRA="cu129" + +mkdir -p "${BIN_DIR}" +exec > >(tee -a "${LOG_FILE}") 2>&1 + +log() { printf '\n=== [nss-setup] %s\n' "$*"; } +fail() { + printf '\n!!! [nss-setup] FAILED at line %s\n' "$1" >&2 + # Truthful, rather than leaving the wait notice up forever. + cat >"${WAIT_FILE}" <"${WAIT_FILE}" <<'EOF' +# Setting up -- please wait + +NeMo Safe Synthesizer is still installing -- roughly 5-10 minutes from when +the instance started. Files appear as it progresses, so a partly-filled file +browser is expected. Nothing here is ready to run yet. + +When setup finishes, this file is replaced by README.md. Refresh to check. +EOF + +export PATH="${BIN_DIR}:${PATH}" + +log "user=$(id -un) home=${HOME}" + +# Preflight: this Launchable only makes sense on an NVIDIA GPU instance. + +if command -v nvidia-smi >/dev/null 2>&1; then + nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv,noheader || true +else + log "WARNING: nvidia-smi not found. Training and generation will not work." +fi + +# uv -- installed into ~/.local/bin, which needs no privileges. + +if [[ "$(uv --version 2>/dev/null | awk '{print $2}')" != "${UV_VERSION}" ]]; then + log "installing uv ${UV_VERSION} into ${BIN_DIR}" + # Verify the published SHA-256; astral's installer verifies nothing. + case "$(uname -m)" in + x86_64) uv_target="x86_64-unknown-linux-gnu" ;; + aarch64 | arm64) uv_target="aarch64-unknown-linux-gnu" ;; + *) + log "ERROR: unsupported architecture $(uname -m)" + exit 1 + ;; + esac + uv_dir="$(mktemp -d)" + uv_url="https://github.com/astral-sh/uv/releases/download/${UV_VERSION}/uv-${uv_target}.tar.gz" + curl -fsSL "${uv_url}" -o "${uv_dir}/uv.tar.gz" + uv_want="$(curl -fsSL "${uv_url}.sha256" | awk '{print $1}')" + uv_got="$(sha256sum "${uv_dir}/uv.tar.gz" | awk '{print $1}')" + if [[ "${uv_want}" != "${uv_got}" ]]; then + log "ERROR: uv checksum mismatch (want ${uv_want}, got ${uv_got})" + exit 1 + fi + tar -xzf "${uv_dir}/uv.tar.gz" -C "${uv_dir}" --strip-components=1 + install -m 0755 "${uv_dir}/uv" "${uv_dir}/uvx" "${BIN_DIR}/" + rm -rf "${uv_dir}" +else + log "uv ${UV_VERSION} already present" +fi + +# The cu129 wheels are large and their indexes are not always fast. +export UV_HTTP_TIMEOUT=300 + +# Venv kept separate from Jupyter's, so a failed install cannot break it. + +if [[ ! -x "${VENV_DIR}/bin/python" ]]; then + log "creating venv at ${VENV_DIR} (python ${PYTHON_VERSION})" + uv venv --python "${PYTHON_VERSION}" "${VENV_DIR}" +else + log "venv already present at ${VENV_DIR}" +fi + +if "${VENV_DIR}/bin/safe-synthesizer" --version >/dev/null 2>&1; then + log "safe-synthesizer already installed; skipping package install" +else + NSS_VERSION="$(curl -fsSL https://pypi.org/pypi/nemo-safe-synthesizer/json \ + | "${VENV_DIR}/bin/python" -c 'import json, sys; print(json.load(sys.stdin)["info"]["version"])')" + + # Indexes come from the installed release's pyproject. Match both generated + # names and URLs because static index names do not enforce the CUDA suffix. + pyproject="$(mktemp)" + curl -fsSL "${REPO_URL}/raw/v${NSS_VERSION}/pyproject.toml" -o "${pyproject}" + index_args=() + index_count=0 + while IFS= read -r url; do + [[ -n "${url}" ]] || continue + index_args+=(--index "${url}") + index_count=$((index_count + 1)) + log "index: ${url}" + done < <(env CUDA_EXTRA="${CUDA_EXTRA}" "${VENV_DIR}/bin/python" - "${pyproject}" <<'PY' +import os +import sys +import tomllib + +with open(sys.argv[1], "rb") as handle: + indexes = tomllib.load(handle)["tool"]["uv"]["index"] + +cuda_extra = os.environ["CUDA_EXTRA"] +print( + "\n".join( + index["url"] + for index in indexes + if index["name"].endswith(f"-{cuda_extra}") + or f"/{cuda_extra}" in index["url"] + ) +) +PY + ) + rm -f "${pyproject}" + + # The parse cannot fail the script from a process substitution; count validates. + if [[ "${index_count}" -lt 3 ]]; then + log "ERROR: expected 3+ ${CUDA_EXTRA} indexes in v${NSS_VERSION}, got ${index_count}" + exit 1 + fi + + log "installing nemo-safe-synthesizer ${NSS_VERSION} -- this takes several minutes" + VIRTUAL_ENV="${VENV_DIR}" uv pip install \ + "nemo-safe-synthesizer[${CUDA_EXTRA},engine]==${NSS_VERSION}" \ + "${index_args[@]}" \ + --index-strategy unsafe-best-match +fi + +# Checked separately, or a rerun skips it and the kernel cannot start. +if ! "${VENV_DIR}/bin/python" -c "import ipykernel, ipywidgets" 2>/dev/null; then + log "installing notebook support" + VIRTUAL_ENV="${VENV_DIR}" uv pip install ipykernel ipywidgets +fi + +# Tutorials: tarball extract pinned to the installed release. See README. + +if [[ -f "${TUTORIALS_DIR}/.fetched" ]]; then + log "tutorials already present" +else + # Set above if this run installed; read off the package if the install ran previously. + NSS_VERSION="${NSS_VERSION:-$("${VENV_DIR}/bin/python" -c \ + 'from importlib.metadata import version; print(version("nemo-safe-synthesizer"))')}" + log "fetching tutorials for version ${NSS_VERSION}" + + # Staged under $HOME: same-filesystem rename, and never half-visible. + tarball_dir="$(mktemp -d "${HOME}/.nss-fetch.XXXXXX")" + tarball="${tarball_dir}/src.tar.gz" + stage="${tarball_dir}/stage" + fetched=0 + + for ref in "refs/tags/v${NSS_VERSION}" "refs/heads/main"; do + if ! curl -fsSL "${REPO_URL}/archive/${ref}.tar.gz" -o "${tarball}"; then + log "WARNING: ${ref} not downloadable; trying next ref" + continue + fi + + # Exact member path, and no pipe to head: GNU/BSD tar traps. See README. + listing="$(tar -tzf "${tarball}")" + top="${listing%%/*}" + rm -rf "${stage}" + mkdir -p "${stage}" + if tar -xzf "${tarball}" -C "${stage}" --strip-components=3 \ + "${top}/docs/tutorials"; then + rm -rf "${TUTORIALS_DIR}" + mv "${stage}" "${TUTORIALS_DIR}" + # Written last; the guard keys on this, so partial runs are redone. + : >"${TUTORIALS_DIR}/.fetched" + log "tutorials extracted from ${ref}" + # Same tarball as the tutorials. Non-fatal -- see README. + if tar -xzf "${tarball}" -C "${tarball_dir}" --strip-components=3 \ + "${top}/script/brev/welcome.md" 2>/dev/null; then + mv "${tarball_dir}/welcome.md" "${WELCOME_STAGED}" + else + log "WARNING: welcome.md not present in ${ref}" + fi + fetched=1 + break + fi + log "WARNING: no tutorials in ${ref}; trying next ref" + done + + rm -rf "${tarball_dir}" + if [[ "${fetched}" -ne 1 ]]; then + log "ERROR: could not fetch tutorial notebooks" + exit 1 + fi +fi + +# Environment for terminals; the kernelspec below covers notebooks. + +log "writing ${ENV_FILE}" +{ + echo "# Managed by setup.sh -- do not edit." + echo "export PATH=\"${VENV_DIR}/bin:${BIN_DIR}:\${PATH}\"" + # Pin VIRTUAL_ENV or uv walks up and finds Brev's own ~/.venv instead. + echo "export VIRTUAL_ENV=\"${VENV_DIR}\"" + if [[ -n "${NSS_INFERENCE_KEY:-}" ]]; then + printf 'export NSS_INFERENCE_KEY=%q\n' "${NSS_INFERENCE_KEY}" + fi + if [[ -n "${HF_TOKEN:-}" ]]; then + printf 'export HF_TOKEN=%q\n' "${HF_TOKEN}" + printf 'export HUGGING_FACE_HUB_TOKEN=%q\n' "${HF_TOKEN}" + fi +} >"${ENV_FILE}" +chmod 0600 "${ENV_FILE}" + +if ! grep -qF "${ENV_FILE}" "${HOME}/.bashrc" 2>/dev/null; then + printf '\n[ -f %s ] && . %s\n' "${ENV_FILE}" "${ENV_FILE}" >>"${HOME}/.bashrc" +fi + +# Kernel registered as `python3` (the name the notebooks declare), in the +# directory the server actually consults -- not the user one. See README. + +log "building kernelspec" +KERNEL_JSON="$(mktemp)" +# `env`, because bash rejects prefix assignments to readonly variables. +env VENV_DIR="${VENV_DIR}" BIN_DIR="${BIN_DIR}" \ + "${VENV_DIR}/bin/python" - "${KERNEL_JSON}" <<'PY' +import json +import os +import sys + +venv = os.environ["VENV_DIR"] +env = { + "PATH": f"{venv}/bin:{os.environ['BIN_DIR']}:/usr/local/bin:/usr/bin:/bin", + # Keeps `!uv pip install ...` in a notebook from resolving to the Brev + # image's own ~/.venv, which uv would otherwise discover by walking up. + "VIRTUAL_ENV": venv, +} +# Secrets live in the kernelspec because the Jupyter server is not launched +# from a login shell. The VM is single-tenant and the file is mode 0600. +for key in ("NSS_INFERENCE_KEY", "HF_TOKEN"): + value = os.environ.get(key) + if value: + env[key] = value +if "HF_TOKEN" in env: + env["HUGGING_FACE_HUB_TOKEN"] = env["HF_TOKEN"] + +spec = { + "argv": [f"{venv}/bin/python", "-m", "ipykernel_launcher", "-f", "{connection_file}"], + "display_name": "Safe Synthesizer", + "language": "python", + "env": env, +} +with open(sys.argv[1], "w", encoding="utf-8") as handle: + json.dump(spec, handle, indent=2) +PY + +# Find the interpreter the Jupyter server runs on, so we can ask it -- rather +# than guess -- where its highest-precedence kernels directory is. +JUPYTER_PY="" +if command -v jupyter >/dev/null 2>&1; then + candidate="$(dirname "$(command -v jupyter)")/python" + if [[ -x "${candidate}" ]]; then + JUPYTER_PY="${candidate}" + fi +fi +if [[ -z "${JUPYTER_PY}" && -x "${HOME}/.venv/bin/python" ]]; then + JUPYTER_PY="${HOME}/.venv/bin/python" +fi + +kernel_targets=("${USER_KERNEL_DIR}") +if [[ -n "${JUPYTER_PY}" ]]; then + primary="$("${JUPYTER_PY}" -c \ + 'from jupyter_core.paths import jupyter_path; print(jupyter_path("kernels")[0])' \ + 2>/dev/null || true)" + if [[ -n "${primary}" ]]; then + kernel_targets=("${primary}/python3" "${kernel_targets[@]}") + fi +else + log "WARNING: could not locate the Jupyter interpreter; kernel may not be picked up" +fi + +# First writable target wins. Writing both would leave a duplicate python3 +# kernelspec that never gets consulted and only confuses `kernelspec list`. +registered=0 +for target in "${kernel_targets[@]}"; do + # `mkdir -p` succeeds on an existing directory even without write permission, + # so test writability separately -- otherwise an unwritable first target + # reaches `cp`, and `set -e` aborts the run instead of trying the next one. + mkdir -p "${target}" 2>/dev/null || { log "WARNING: cannot create ${target}"; continue; } + if [[ ! -w "${target}" ]]; then + log "WARNING: ${target} is not writable" + continue + fi + # Keep whatever was there, once, so the original kernel stays recoverable. + if [[ -f "${target}/kernel.json" && ! -f "${target}/kernel.json.orig" ]]; then + cp "${target}/kernel.json" "${target}/kernel.json.orig" 2>/dev/null || true + fi + cp "${KERNEL_JSON}" "${target}/kernel.json" 2>/dev/null || { + log "WARNING: cannot write ${target}/kernel.json" + continue + } + chmod 0600 "${target}/kernel.json" + log "registered kernel at ${target}" + registered=1 + break +done +rm -f "${KERNEL_JSON}" +if [[ "${registered}" -ne 1 ]]; then + log "WARNING: kernel not registered; notebooks may open on the wrong Python" +fi + +# Smoke check -- fail provisioning loudly rather than handing over a broken VM. + +log "verifying install" +"${VENV_DIR}/bin/safe-synthesizer" --version +"${VENV_DIR}/bin/python" \ + -c "import torch; print('cuda available:', torch.cuda.is_available())" + +# Hand over: swap the "please wait" file for the welcome text. + +if [[ -f "${WELCOME_STAGED}" ]]; then + mv "${WELCOME_STAGED}" "${README_FILE}" +else + log "WARNING: no welcome.md staged; skipping ${README_FILE}" +fi +rm -f "${WAIT_FILE}" + +trap - ERR +log "setup complete" +cat < + + +# NeMo Safe Synthesizer + +Create private, safe versions of sensitive tabular data -- entirely synthetic +records with no one-to-one mapping back to your originals. + +Everything is installed and ready. Nothing to set up. + +## Start here + +Open `tutorials/safe-synthesizer-101.ipynb` and run the cells top to bottom. +It takes about 15 minutes and walks through the full pipeline on a sample +dataset. + +The other two notebooks go deeper: + +- `tutorials/differential-privacy.ipynb` -- formal privacy guarantees (~1 hour) +- `tutorials/time-series-financial-transactions.ipynb` -- sequential data (~20 minutes) + +## Using your own data + +Upload a CSV anywhere you like -- drag and drop into the file browser on the +left -- and point the notebook at it: + +```python +from nemo_safe_synthesizer import SafeSynthesizer + +results = SafeSynthesizer().with_data_source("my-data.csv").run() +``` + +There is no required location for your files. Put them wherever suits you. + +## Good to know + +- Notebooks already run on the right Python. You should never need to change + the kernel; if you do switch it, pick "Safe Synthesizer". +- Model weights download on first use, so the first run is slower than later + ones. +- This instance bills continuously and cannot be paused. Delete it when you are + finished, and download anything you want to keep first. +- Provisioning log: `.nss-setup.log` (hidden files are off by default in the + file browser). + +## Learn more + +- Documentation: +- Source: