Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 1 addition & 1 deletion docs/user-guide/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ docker run --gpus all --shm-size=1g \
nss-gpu:latest run --config /workspace/data/config.yaml --data-source /workspace/data/input.csv
```

See [Environment Variables -- Hugging Face Cache](environment.md#hugging-face-cache)
See [Environment Variables -- Hugging Face cache and offline](environment.md#hugging-face-cache-and-offline)
for details on `HF_HOME`, `HF_HUB_OFFLINE`, and `VLLM_CACHE_ROOT`.

---
Expand Down
349 changes: 175 additions & 174 deletions docs/user-guide/environment.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions docs/user-guide/running.md
Original file line number Diff line number Diff line change
Expand Up @@ -1231,9 +1231,9 @@ See [`artifacts clean`](#artifacts-clean) in the CLI Commands section for option

Pre-cache models by running once with internet access, then set
`HF_HUB_OFFLINE=1` in your target environment. For detailed cache setup
and environment variables (`HF_HOME`, `HF_HUB_OFFLINE`, `LOCAL_FILES_ONLY`,
and environment variables (`HF_HOME`, `HF_HUB_OFFLINE`, `NSS_LOCAL_FILES_ONLY`,
`VLLM_CACHE_ROOT`), see
[Environment Variables -- Hugging Face Cache](environment.md#hugging-face-cache).
[Environment Variables -- Hugging Face cache and offline](environment.md#hugging-face-cache-and-offline).

For offline-specific errors, see [Program Runtime](troubleshooting.md).

Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ The PII replacer downloads the GLiNER NER model on first use. If the download
fails, it raises an exception immediately.

Pre-download the model by running PII replacement once in an environment
with internet access, or set `LOCAL_FILES_ONLY=true` after the model is cached.
with internet access, or set `NSS_LOCAL_FILES_ONLY=true` after the model is cached.

### NER Processing Timeouts

Expand Down
2 changes: 1 addition & 1 deletion script/slurm/slurm_nss_matrix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ echo "[NSS SLURM] nemo-safe-synthesizer version: $(python -c 'from nemo_safe_syn

# for column classification
export NSS_INFERENCE_ENDPOINT=https://integrate.api.nvidia.com/v1
export NIM_MODEL_ID=qwen/qwen3-next-80b-a3b-instruct
export NSS_INFERENCE_MODEL=qwen/qwen3-next-80b-a3b-instruct

# Extract dataset name for path construction (handles both full paths and simple names)
# e.g., "/path/to/adult.csv" -> "adult", "/path/to/data.parquet" -> "data", "adult" -> "adult"
Expand Down
167 changes: 83 additions & 84 deletions src/nemo_safe_synthesizer/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,58 @@ def common_run_options(f: Callable[..., object]) -> Callable[..., object]:
"If both env var and CLI option are provided, the CLI option takes precedence.",
)
)
options.append(
click.option(
"--inference-endpoint-url",
type=str,
required=False,
default=None,
help="OpenAI-compatible inference endpoint URL for PII column classification. "
"Can also be set via NSS_INFERENCE_ENDPOINT env var.",
)
)
options.append(
click.option(
"--inference-api-key",
type=str,
required=False,
default=None,
help="API key for the inference endpoint used in PII column classification. "
"Can also be set via NSS_INFERENCE_KEY env var.",
)
)
Comment on lines +172 to +181

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security API key exposed in process listings and shell history

--inference-api-key is declared as a plain type=str option. Any value passed on the command line is visible in /proc/PID/cmdline, ps aux, shell history files, and CI logs that echo commands — all readable by other users on the same host. The canonical env-var path (NSS_INFERENCE_KEY) avoids this; the help text documents it, but does not warn that the flag itself is the insecure route. Passing --inference-api-key my-secret on a shared Slurm node or in a verbose CI step will leak the key to any co-tenant process with /proc access.

options.append(
click.option(
"--inference-model-id",
type=str,
required=False,
default=None,
help="Model ID sent to the inference endpoint for PII column classification. "
"Can also be set via NSS_INFERENCE_MODEL env var. "
"[default: qwen/qwen3-next-80b-a3b-instruct]",
)
)
options.append(
click.option(
"--local-files-only/--no-local-files-only",
"local_files_only",
required=False,
default=None,
Comment thread
binaryaaron marked this conversation as resolved.
help="If set, GLiNER skips network downloads and uses only local files. "
"Can also be set via NSS_LOCAL_FILES_ONLY env var.",
)
Comment thread
binaryaaron marked this conversation as resolved.
)
options.append(
click.option(
"--cpu-count",
type=int,
required=False,
default=None,
help="Number of CPU worker processes used for NER (PII replacement). "
"Can also be set via NSS_PII_REPLACER_CPU_COUNT env var. "
"[default: max(1, cpu_count - 1)]",
)
)
# Apply each option decorator in reverse order (decorators apply bottom-up)
for option in reversed(options):
f = option(f)
Expand All @@ -170,6 +222,31 @@ def _parse_run_overrides(kwargs: dict[str, Any]) -> dict[str, Any]:
return parse_overrides(kwargs)


# CLISettings fields populated from common_run_options flags. ``synthesis_overrides``
# is excluded -- it is derived from the leftover pydantic_options kwargs, not bound
# to a single flag. ``observability``/``wandb`` are nested sub-settings with no CLI
# flag, so they never appear in command kwargs.
_CLI_SETTINGS_FIELDS: frozenset[str] = frozenset(CLISettings.model_fields) - {"synthesis_overrides"}


def _settings_from_run_kwargs(kwargs: dict[str, Any]) -> CLISettings:
"""Build ``CLISettings`` from a run command's ``**kwargs``.

``common_run_options`` binds each infrastructure flag to a kwarg whose name
matches a ``CLISettings`` field; those are pulled out here. Everything left
(the ``pydantic_options`` ``--section__field`` options) becomes synthesis
overrides. This keeps the three run commands from re-listing the shared flag
set in both their signature and their settings construction -- adding a flag
now means editing ``common_run_options`` and ``CLISettings`` only.

``kwargs`` is mutated: matched settings keys are popped before the remainder
is parsed into overrides.
"""
settings_kwargs = {name: kwargs.pop(name) for name in _CLI_SETTINGS_FIELDS if name in kwargs}
settings_kwargs["synthesis_overrides"] = _parse_run_overrides(kwargs)
return CLISettings.from_cli_kwargs(**settings_kwargs)


def _set_cli_deployment_type_default() -> None:
"""Default telemetry deployment type for CLI commands without overriding Slurm or explicit settings."""
os.environ.setdefault("NEMO_DEPLOYMENT_TYPE", DeploymentTypeEnum.CLI.value)
Expand Down Expand Up @@ -311,20 +388,8 @@ def _build_validate_render_context(
)
def run(
ctx: click.Context,
config_path: PathT | None,
data_source: str | None,
artifact_path: PathT | None,
run_path: PathT | None,
output_file: PathT | None,
log_file: PathT | None,
log_color: bool | None,
log_format: str | None,
verbose: int = 0,
wandb_mode: str | None = None,
wandb_project: str | None = None,
dataset_registry: str | None = None,
validate: bool = False,
**kwargs: object,
**kwargs: Any,
) -> None:
"""Run the Safe Synthesizer end-to-end pipeline.

Expand All @@ -337,21 +402,7 @@ def run(

_set_cli_deployment_type_default()

settings = CLISettings.from_cli_kwargs(
data_source=data_source,
config_path=config_path,
artifact_path=artifact_path,
run_path=run_path,
output_file=output_file,
log_file=log_file,
log_color=log_color,
log_format=log_format,
verbose=verbose,
wandb_mode=wandb_mode,
wandb_project=wandb_project,
synthesis_overrides=_parse_run_overrides(kwargs),
dataset_registry=dataset_registry,
)
settings = _settings_from_run_kwargs(kwargs)

if validate:
os.environ["NSS_PHASE"] = "process_data"
Expand Down Expand Up @@ -407,20 +458,8 @@ def run(
help="Run pre-flight validation only, then exit without training or generating.",
)
def run_train(
config_path: PathT,
data_source: str | None,
artifact_path: PathT | None,
run_path: PathT | None,
output_file: PathT | None,
log_format: str | None,
log_color: bool | None,
log_file: PathT | None,
verbose: int,
wandb_mode: str | None = None,
wandb_project: str | None = None,
dataset_registry: str | None = None,
validate: bool = False,
**kwargs: object,
**kwargs: Any,
Comment thread
binaryaaron marked this conversation as resolved.
) -> None:
"""Run the training stage only.

Expand All @@ -429,21 +468,7 @@ def run_train(
"""
_set_cli_deployment_type_default()

settings = CLISettings.from_cli_kwargs(
data_source=data_source,
config_path=config_path,
artifact_path=artifact_path,
run_path=run_path,
output_file=output_file,
log_file=log_file,
log_color=log_color,
log_format=log_format,
verbose=verbose,
wandb_mode=wandb_mode,
wandb_project=wandb_project,
synthesis_overrides=_parse_run_overrides(kwargs),
dataset_registry=dataset_registry,
)
settings = _settings_from_run_kwargs(kwargs)

if validate:
os.environ["NSS_PHASE"] = "process_data"
Expand Down Expand Up @@ -498,21 +523,9 @@ def run_train(
)
@pydantic_options(SafeSynthesizerParameters, field_separator=CLI_NESTED_FIELD_SEPARATOR)
def run_generate(
config_path: PathT,
data_source: str | None,
run_path: PathT | None,
artifact_path: PathT | None,
output_file: PathT | None,
log_format: str | None,
log_color: bool | None,
log_file: PathT | None,
verbose: int,
wandb_mode: str | None = None,
wandb_project: str | None = None,
auto_discover_adapter: bool = False,
wandb_resume_job_id: str | None = None,
dataset_registry: str | None = None,
**kwargs: object,
**kwargs: Any,
) -> None:
"""Run the generation stage only.

Expand All @@ -526,21 +539,7 @@ def run_generate(
_set_cli_deployment_type_default()

# Create unified settings from CLI kwargs
settings = CLISettings.from_cli_kwargs(
data_source=data_source,
config_path=config_path,
artifact_path=artifact_path,
run_path=run_path,
output_file=output_file,
log_file=log_file,
log_color=log_color,
log_format=log_format,
verbose=verbose,
wandb_mode=wandb_mode,
wandb_project=wandb_project,
synthesis_overrides=_parse_run_overrides(kwargs),
dataset_registry=dataset_registry,
)
settings = _settings_from_run_kwargs(kwargs)

os.environ["NSS_PHASE"] = "generate"
# Generation always resumes from an existing workdir with a trained model
Expand Down
43 changes: 42 additions & 1 deletion src/nemo_safe_synthesizer/cli/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,10 @@ class CLISettings(BaseSettings):

log_color: bool | None = Field(
default=None,
validation_alias=AliasChoices("log_color", "NSS_LOG_COLOR"),
description="Whether to colorize console output",
)
"""Whether to colorize console output."""
"""Whether to colorize console output (env variable: ``NSS_LOG_COLOR``)."""

log_file: str | None = Field(
default=None,
Expand Down Expand Up @@ -164,6 +165,46 @@ class CLISettings(BaseSettings):
)
"""URL or path to a dataset registry YAML file (env: ``NSS_DATASET_REGISTRY``)."""

inference_endpoint_url: str | None = Field(
default=None,
validation_alias=AliasChoices("inference_endpoint_url", "NSS_INFERENCE_ENDPOINT"),
description="OpenAI-compatible inference endpoint URL for PII column classification",
)
"""OpenAI-compatible inference endpoint URL for PII column classification
(env: ``NSS_INFERENCE_ENDPOINT``)."""

inference_api_key: str | None = Field(
default=None,
validation_alias=AliasChoices("inference_api_key", "NSS_INFERENCE_KEY"),
description="API key for the inference endpoint used in PII column classification",
)
"""API key for the inference endpoint used in PII column classification
(env: ``NSS_INFERENCE_KEY``)."""

inference_model_id: str | None = Field(
default=None,
validation_alias=AliasChoices("inference_model_id", "NSS_INFERENCE_MODEL"),
description="Model ID sent to the inference endpoint for PII column classification",
)
"""Model ID sent to the inference endpoint for PII column classification
(env: ``NSS_INFERENCE_MODEL``)."""

local_files_only: bool | None = Field(
default=None,
validation_alias=AliasChoices("local_files_only", "NSS_LOCAL_FILES_ONLY"),
description="Whether GLiNER should skip network downloads and use only local files",
)
"""Whether GLiNER should skip network downloads and use only local files (env: ``NSS_LOCAL_FILES_ONLY``)."""

cpu_count: int | None = Field(
default=None,
ge=1,
validation_alias=AliasChoices("cpu_count", "NSS_PII_REPLACER_CPU_COUNT"),
description="Number of CPU worker processes used for NER (PII replacement)",
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
binaryaaron marked this conversation as resolved.
"""Number of CPU worker processes used for NER (PII replacement)
(env: ``NSS_PII_REPLACER_CPU_COUNT``)."""

@field_validator("wandb_mode", mode="before")
@classmethod
def validate_wandb_mode(cls, v: str | WandbMode | None) -> WandbMode | None:
Expand Down
35 changes: 35 additions & 0 deletions src/nemo_safe_synthesizer/cli/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,12 @@ def common_setup(
Tuple of (logger, config, dataframe, workdir). For generate-only runs with
cached datasets, dataframe may be None (loaded from cached files by SafeSynthesizer).
"""
# 0. Propagate CLI-resolved runtime settings back to os.environ. This must
# run before any deferred pii_replacer imports so that module-level reads
# of NSS_INFERENCE_*, NSS_LOCAL_FILES_ONLY, and NSS_PII_REPLACER_CPU_COUNT
# see the CLI-overridden values.
_propagate_runtime_settings_to_env(settings)

# 1. Create workdir FIRST - this establishes all artifact paths
workdir = _create_workdir(
settings.artifact_path,
Expand Down Expand Up @@ -314,6 +320,35 @@ def _set_wandb_env_vars(
os.environ["WANDB_RUN_NAME"] = wandb_run_name


def _propagate_runtime_settings_to_env(settings: "CLISettings") -> None:
"""Materialize CLI-resolved runtime settings back to ``os.environ``.

The downstream readers for these settings live deep in ``pii_replacer``
Comment thread
binaryaaron marked this conversation as resolved.
(NER, GLiNER, column classification) and historically read directly from
the process environment. Rather than thread a ``CLISettings`` handle
through every callsite, we propagate the resolved values back to
``os.environ`` here so that CLI flag precedence -- which ``CLISettings``
handles via ``from_cli_kwargs`` -- carries through to those readers
unchanged.

``CLISettings`` values are already env-aware (via ``AliasChoices``); when
no CLI flag is provided, the field carries the env var's existing value
and writing it back is a no-op. When a CLI flag overrides the env var,
this overwrites ``os.environ`` so the deferred imports in the runtime
pipeline see the CLI value.
"""
if settings.inference_endpoint_url is not None:
os.environ["NSS_INFERENCE_ENDPOINT"] = settings.inference_endpoint_url
if settings.inference_api_key is not None:
os.environ["NSS_INFERENCE_KEY"] = settings.inference_api_key
if settings.inference_model_id is not None:
os.environ["NSS_INFERENCE_MODEL"] = settings.inference_model_id
if settings.local_files_only is not None:
os.environ["NSS_LOCAL_FILES_ONLY"] = "true" if settings.local_files_only else "false"
if settings.cpu_count is not None:
os.environ["NSS_PII_REPLACER_CPU_COUNT"] = str(settings.cpu_count)


def _initialize_logging_for_cli_from_settings(
settings: "CLISettings",
workdir: Workdir,
Expand Down
Loading
Loading