Skip to content

Commit 8f50068

Browse files
committed
feat(cli): add InferenceModelCheck and keep CLI import hub-free for offline flag
Consolidate inference env validation into a single preflight check and make the Hugging Face offline switch reliable end to end. - preflight: rename InferenceKeyCheck to InferenceModelCheck (env.inference); validate NSS_INFERENCE_KEY, NSS_INFERENCE_MODEL, and NSS_INFERENCE_ENDPOINT via single-dispatch match logic. - cli: replace --local-files-only with --enable/--disable-huggingface-remote (CLI-only, no NSS env var); propagate to HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE. - utils: add shared hf_offline_enabled() and env_flag_is_true(); detect.py reads NSS_INFERENCE_MODEL at call time and GLiNER offline from env. - imports: defer huggingface_hub in telemetry and datasets in utils so the cli.cli import chain stays hub-free; HF_HUB_OFFLINE is then propagated before huggingface_hub first loads. Add tests/cli/test_cli_import regression guard. - docs: document the offline switch, CLI flag precedence, and import-time caching of HF_HUB_OFFLINE. Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
1 parent c1ab315 commit 8f50068

19 files changed

Lines changed: 361 additions & 96 deletions

File tree

docs/user-guide/environment.md

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,10 @@ Grouped by the `Category` column -- `nss`-native settings first, then
5050
| `NSS_INFERENCE_ENDPOINT` | nss | `--inference-endpoint-url` | PII column classifier | NVIDIA integrate URL | OpenAI-compatible endpoint for column classification | [PII appendix](#pii-ner-and-column-classification) |
5151
| `NSS_INFERENCE_KEY` | nss | `--inference-api-key` | PII column classifier | -- | API key for `NSS_INFERENCE_ENDPOINT` | Required for LLM column classification |
5252
| `NSS_INFERENCE_MODEL` | nss | `--inference-model-id` | PII column classifier | `qwen/qwen3-next-80b-a3b-instruct` | Model ID sent to the inference endpoint | [PII appendix](#pii-ner-and-column-classification) |
53-
| `NSS_LOCAL_FILES_ONLY` | nss | `--local-files-only` / `--no-local-files-only` | GLiNER (PII) | unset | Skip GLiNER network downloads | Partial offline; see [HF appendix](#hugging-face-cache-and-offline) |
5453
| `NSS_PII_REPLACER_CPU_COUNT` | nss | `--cpu-count` | NER worker pool | `max(1, cpu_count - 1)` | CPU processes for PII NER | [PII appendix](#pii-ner-and-column-classification) |
5554
| `NEMO_TELEMETRY_ENABLED` | telemetry | `--emit_telemetry` | telemetry | `true` | Enable anonymous usage telemetry | Also `emit_telemetry` in YAML; see [Telemetry](#telemetry) |
5655
| `HF_HOME` | third-party | -- | Hugging Face Hub | platform cache dir | Root directory for HF downloads | [HF appendix](#hugging-face-cache-and-offline) |
57-
| `HF_HUB_OFFLINE` | third-party | -- | Hugging Face Hub | unset | Fail if a model is not cached | Preferred offline gate |
56+
| `HF_HUB_OFFLINE` | third-party | `--enable-huggingface-remote` / `--disable-huggingface-remote` | Hugging Face Hub | unset | Fail if a model is not cached (covers base model and GLiNER) | Preferred offline gate; CLI flag also sets `TRANSFORMERS_OFFLINE` |
5857
| `VLLM_CACHE_ROOT` | third-party | -- | vLLM | `~/.cache/vllm` | vLLM model cache directory | [vLLM appendix](#vllm-and-attention) |
5958
| `VLLM_ATTENTION_BACKEND` | third-party | -- | vLLM | auto | Override attention implementation | [vLLM appendix](#vllm-and-attention) |
6059
| `WANDB_MODE` | third-party | `--wandb-mode` | WandB | `disabled` | WandB run mode | Same as `NSS_WANDB_MODE` |
@@ -74,7 +73,8 @@ Grouped by the `Category` column -- `nss`-native settings first, then
7473
### Infrastructure (CLISettings)
7574

7675
For artifact paths, logging, WandB overrides, and the five runtime flags
77-
(`--inference-*`, `--local-files-only`, `--cpu-count`):
76+
(`--inference-*`, `--enable-huggingface-remote` / `--disable-huggingface-remote`,
77+
`--cpu-count`):
7878

7979
1. CLI flags
8080
2. Environment variables
@@ -115,29 +115,41 @@ export HF_HOME=/shared/cache/huggingface
115115

116116
### `HF_HUB_OFFLINE`
117117

118-
When set to `1`, Hugging Face Hub refuses network access. Use with a
119-
pre-populated `HF_HOME` for reliable offline runs.
118+
`HF_HUB_OFFLINE=1` tells Hugging Face Hub to refuse network access. It is the
119+
canonical offline switch: huggingface_hub honors it globally, so a single
120+
setting covers both the base model and GLiNER. Pair it with a pre-populated
121+
`HF_HOME`.
120122

121123
```bash
122124
export HF_HUB_OFFLINE=1
123125
```
124126

125-
Prefer this over `NSS_LOCAL_FILES_ONLY` for end-to-end offline behavior.
127+
Set it before the process starts. huggingface_hub reads the value once, when it
128+
is first imported, and caches it -- changing it later has no effect for that
129+
process. For the CLI, export it before launching `safe-synthesizer`. When
130+
driving the pipeline programmatically, set it before importing
131+
`nemo_safe_synthesizer`.
126132

127-
### `NSS_LOCAL_FILES_ONLY`
133+
### `--enable-huggingface-remote` / `--disable-huggingface-remote`
128134

129-
Skips network downloads for GLiNER only. Not respected by the HuggingFace
130-
training backend or vLLM. Override on the CLI with `--local-files-only` or
131-
`--no-local-files-only`.
135+
CLI shorthand for the switch above, with no separate NSS env var:
136+
137+
- `--disable-huggingface-remote` -- offline run; sets `HF_HUB_OFFLINE=1` and
138+
`TRANSFORMERS_OFFLINE=1`.
139+
- `--enable-huggingface-remote` -- online run; sets both to `0`, overriding any
140+
inherited offline environment.
141+
142+
The CLI applies the flag before huggingface_hub loads, so the flag always wins
143+
over an inherited environment value. For env-based control, set `HF_HUB_OFFLINE`
144+
directly.
132145

133146
```bash
134-
export NSS_LOCAL_FILES_ONLY=true
147+
safe-synthesizer run --disable-huggingface-remote ...
135148
```
136149

137-
!!! warning "Partial offline support"
138-
For the most reliable offline experience, set `HF_HUB_OFFLINE=1` with a
139-
pre-populated `HF_HOME` cache instead of relying on `NSS_LOCAL_FILES_ONLY`
140-
alone.
150+
!!! warning "Models must be cached"
151+
Offline mode requires the base model and GLiNER to already be present in
152+
`HF_HOME`. Loading fails if a required model is not cached.
141153

142154
### Pre-caching models
143155

docs/user-guide/running.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ execute in order (`config` → `dataframe` → `metadata` → `advisory`).
274274
| Check name | Stage | What it validates |
275275
|-------|-------|-------------------|
276276
| `gpu.cuda` | config | PyTorch is importable and a CUDA GPU is visible |
277-
| `env.inference_key` | config | `NSS_INFERENCE_KEY` is set when PII classification is enabled (warning only) |
277+
| `env.inference` | config | Inference config for PII classification: `NSS_INFERENCE_KEY` is set, `NSS_INFERENCE_MODEL` is non-empty, and `NSS_INFERENCE_ENDPOINT` is a valid http(s) URL (warnings only) |
278278
| `env.hf_model_availability` | config | The pretrained model reference is usable locally or can be fetched from Hugging Face; warns about a missing HF token only when online HF access may be needed |
279279
| `dataset.size` | dataframe | Training split meets the hard minimum row count |
280280
| `columns.groupby` | dataframe | `group_training_examples_by` column is present and has no nulls |
@@ -1230,9 +1230,11 @@ See [`artifacts clean`](#artifacts-clean) in the CLI Commands section for option
12301230
## Running in Offline Environments
12311231

12321232
Pre-cache models by running once with internet access, then set
1233-
`HF_HUB_OFFLINE=1` in your target environment. For detailed cache setup
1234-
and environment variables (`HF_HOME`, `HF_HUB_OFFLINE`, `NSS_LOCAL_FILES_ONLY`,
1235-
`VLLM_CACHE_ROOT`), see
1233+
`HF_HUB_OFFLINE=1` in your target environment. Export it before launching
1234+
`safe-synthesizer` (or pass `--disable-huggingface-remote`) -- huggingface_hub
1235+
reads the value once at import time, so setting it after the process starts has
1236+
no effect. For detailed cache setup and environment variables (`HF_HOME`,
1237+
`HF_HUB_OFFLINE`, `VLLM_CACHE_ROOT`), see
12361238
[Environment Variables -- Hugging Face cache and offline](environment.md#hugging-face-cache-and-offline).
12371239

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

docs/user-guide/troubleshooting.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,9 @@ check of its own.
513513
| `torch_missing` | error | `gpu.cuda` | PyTorch not installed; cannot verify GPU availability |
514514
| `no_gpu` | error | `gpu.cuda` | No CUDA GPU detected (required for training or generation) |
515515
| `low_vram` | warning | `gpu.vram` | Free GPU VRAM may be insufficient |
516-
| `inference_key_missing` | warning | `env.inference_key` | `NSS_INFERENCE_KEY` not set; PII classification degraded |
516+
| `inference_key_missing` | warning | `env.inference` | `NSS_INFERENCE_KEY` not set; PII classification degraded |
517+
| `inference_model_blank` | warning | `env.inference` | `NSS_INFERENCE_MODEL` set but empty; classification would send an empty model id and fail |
518+
| `inference_endpoint_invalid` | warning | `env.inference` | `NSS_INFERENCE_ENDPOINT` set but not a valid http(s) URL; classification requests will fail |
517519
| `hf_token_missing` | warning | `env.hf_model_availability` | Neither `HF_TOKEN` nor `HUGGING_FACE_HUB_TOKEN` set, and model loading may need online Hugging Face access |
518520
| `hf_model_not_cached` | warning/error | `env.hf_model_availability` | Hugging Face model is not present in the local cache; severity is error when HF offline mode is enabled |
519521
| `hf_model_cache_incomplete` | warning/error | `env.hf_model_availability` | Cached Hugging Face model snapshot is missing required config, tokenizer, weights, or shards; severity is error when HF offline mode is enabled |
@@ -545,7 +547,29 @@ The PII replacer downloads the GLiNER NER model on first use. If the download
545547
fails, it raises an exception immediately.
546548

547549
Pre-download the model by running PII replacement once in an environment
548-
with internet access, or set `NSS_LOCAL_FILES_ONLY=true` after the model is cached.
550+
with internet access. To force offline use after the model is cached, set
551+
`HF_HUB_OFFLINE=1` or pass `--disable-huggingface-remote`.
552+
553+
### Offline Mode Not Taking Effect
554+
555+
Symptom: `HF_HUB_OFFLINE=1` (or `--disable-huggingface-remote`) is set, yet the
556+
run still attempts a download, or `--enable-huggingface-remote` does not
557+
re-enable downloads.
558+
559+
Cause: huggingface_hub reads `HF_HUB_OFFLINE` once, at import time, and caches
560+
it. If the variable is changed after huggingface_hub has been imported in the
561+
process, the change is ignored.
562+
563+
Fixes:
564+
565+
- CLI: export `HF_HUB_OFFLINE` before launching `safe-synthesizer`, or use
566+
`--enable-huggingface-remote` / `--disable-huggingface-remote`. The CLI
567+
applies the flag before huggingface_hub loads, so the flag always wins over
568+
an inherited environment value.
569+
- Programmatic / SDK: set `HF_HUB_OFFLINE` before importing
570+
`nemo_safe_synthesizer` (or any library that imports huggingface_hub, such as
571+
`transformers` or `datasets`). Setting it afterward has no effect for that
572+
process.
549573

550574
### NER Processing Timeouts
551575

src/nemo_safe_synthesizer/cli/run.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,14 @@ def common_run_options(f: Callable[..., object]) -> Callable[..., object]:
192192
)
193193
options.append(
194194
click.option(
195-
"--local-files-only/--no-local-files-only",
196-
"local_files_only",
195+
"--enable-huggingface-remote/--disable-huggingface-remote",
196+
"huggingface_remote",
197197
required=False,
198198
default=None,
199-
help="If set, GLiNER skips network downloads and uses only local files. "
200-
"Can also be set via NSS_LOCAL_FILES_ONLY env var.",
199+
help="Allow or block Hugging Face remote downloads for both the base model "
200+
"and GLiNER. --disable-huggingface-remote forces a fully offline run by "
201+
"setting HF_HUB_OFFLINE and TRANSFORMERS_OFFLINE; both must already be "
202+
"cached. Equivalent to setting HF_HUB_OFFLINE in the environment.",
201203
)
202204
)
203205
options.append(

src/nemo_safe_synthesizer/cli/settings.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,12 +189,17 @@ class CLISettings(BaseSettings):
189189
"""Model ID sent to the inference endpoint for PII column classification
190190
(env: ``NSS_INFERENCE_MODEL``)."""
191191

192-
local_files_only: bool | None = Field(
192+
huggingface_remote: bool | None = Field(
193193
default=None,
194-
validation_alias=AliasChoices("local_files_only", "NSS_LOCAL_FILES_ONLY"),
195-
description="Whether GLiNER should skip network downloads and use only local files",
194+
validation_alias=AliasChoices("huggingface_remote"),
195+
description="Whether to allow Hugging Face remote downloads (base model and GLiNER)",
196196
)
197-
"""Whether GLiNER should skip network downloads and use only local files (env: ``NSS_LOCAL_FILES_ONLY``)."""
197+
"""Whether to allow Hugging Face remote downloads for the base model and GLiNER.
198+
199+
``None`` leaves the environment untouched. ``True`` / ``False`` is propagated
200+
to the standard ``HF_HUB_OFFLINE`` and ``TRANSFORMERS_OFFLINE`` variables (the
201+
canonical env switch) by ``_propagate_runtime_settings_to_env``; there is no
202+
separate NSS env var."""
198203

199204
cpu_count: int | None = Field(
200205
default=None,

src/nemo_safe_synthesizer/cli/utils.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,8 @@ def common_setup(
232232
"""
233233
# 0. Propagate CLI-resolved runtime settings back to os.environ. This must
234234
# run before any deferred pii_replacer imports so that module-level reads
235-
# of NSS_INFERENCE_*, NSS_LOCAL_FILES_ONLY, and NSS_PII_REPLACER_CPU_COUNT
236-
# see the CLI-overridden values.
235+
# of NSS_INFERENCE_*, HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE, and
236+
# NSS_PII_REPLACER_CPU_COUNT see the CLI-overridden values.
237237
_propagate_runtime_settings_to_env(settings)
238238

239239
# 1. Create workdir FIRST - this establishes all artifact paths
@@ -336,15 +336,31 @@ def _propagate_runtime_settings_to_env(settings: "CLISettings") -> None:
336336
and writing it back is a no-op. When a CLI flag overrides the env var,
337337
this overwrites ``os.environ`` so the deferred imports in the runtime
338338
pipeline see the CLI value.
339+
340+
``huggingface_remote`` is the exception: it has no NSS env var and instead
341+
maps to the standard Hugging Face offline switches (``HF_HUB_OFFLINE`` and
342+
``TRANSFORMERS_OFFLINE``). ``--disable-huggingface-remote`` sets them to
343+
``1``; ``--enable-huggingface-remote`` sets them to ``0`` (overriding any
344+
inherited offline env).
345+
346+
``huggingface_hub`` caches ``HF_HUB_OFFLINE`` at import time, so this write
347+
is only effective if it runs before the first ``huggingface_hub`` import.
348+
The CLI import chain is kept hub-free for exactly this reason --
349+
``telemetry`` defers its ``huggingface_hub`` import (see
350+
``sanitize_model_for_telemetry``) -- so ``huggingface_hub`` first loads
351+
during the pipeline, after this propagation. ``tests/cli/test_cli_import``
352+
guards the hub-free import invariant.
339353
"""
340354
if settings.inference_endpoint_url is not None:
341355
os.environ["NSS_INFERENCE_ENDPOINT"] = settings.inference_endpoint_url
342356
if settings.inference_api_key is not None:
343357
os.environ["NSS_INFERENCE_KEY"] = settings.inference_api_key
344358
if settings.inference_model_id is not None:
345359
os.environ["NSS_INFERENCE_MODEL"] = settings.inference_model_id
346-
if settings.local_files_only is not None:
347-
os.environ["NSS_LOCAL_FILES_ONLY"] = "true" if settings.local_files_only else "false"
360+
if settings.huggingface_remote is not None:
361+
offline = "0" if settings.huggingface_remote else "1"
362+
os.environ["HF_HUB_OFFLINE"] = offline
363+
os.environ["TRANSFORMERS_OFFLINE"] = offline
348364
if settings.cpu_count is not None:
349365
os.environ["NSS_PII_REPLACER_CPU_COUNT"] = str(settings.cpu_count)
350366

src/nemo_safe_synthesizer/pii_replacer/data_editor/detect.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from pydantic import ConfigDict, TypeAdapter, ValidationError
2222

2323
from ...observability import get_logger
24-
from ...utils import env_flag_is_true
24+
from ...utils import hf_offline_enabled
2525
from ..ner import ner_mp
2626
from ..ner.factory import LabelSetPredictorFilter, NERFactory
2727
from ..ner.ner import NERPrediction
@@ -33,12 +33,10 @@
3333
class DefaultLLMConfig:
3434
"""Default settings for the LLM used in column classification.
3535
36-
All attributes are class-level. Used by ``classify_columns`` when calling the
37-
inference API for column-type classification.
36+
Used by ``classify_columns`` when calling the inference API for column-type
37+
classification.
3838
3939
Attributes:
40-
CONFIG_ID: Model identifier for the LLM. From env ``NSS_INFERENCE_MODEL``, or
41-
``qwen/qwen3-next-80b-a3b-instruct`` if unset.
4240
SYSTEM_PROMPT: System message describing the column-type annotation task
4341
sent to the LLM.
4442
MAX_OUTPUT_TOKENS: Maximum number of tokens allowed in the LLM response
@@ -47,11 +45,22 @@ class DefaultLLMConfig:
4745
Lower values give more deterministic output.
4846
"""
4947

50-
CONFIG_ID = os.environ.get("NSS_INFERENCE_MODEL", "qwen/qwen3-next-80b-a3b-instruct")
48+
DEFAULT_CONFIG_ID = "qwen/qwen3-next-80b-a3b-instruct"
5149
SYSTEM_PROMPT = "You are a helpful AI that annotates columns in datasets with their respective types. "
5250
MAX_OUTPUT_TOKENS = 2048
5351
TEMPERATURE = 0.2
5452

53+
@classmethod
54+
def config_id(cls) -> str:
55+
"""Model identifier for the LLM, read from env at call-time.
56+
57+
Reads ``NSS_INFERENCE_MODEL`` on each call (falling back to
58+
``DEFAULT_CONFIG_ID``) so a value set after this module is imported still
59+
takes effect, matching the call-time env handling used elsewhere in this
60+
module.
61+
"""
62+
return os.environ.get("NSS_INFERENCE_MODEL", cls.DEFAULT_CONFIG_ID)
63+
5564

5665
DEFAULT_ENTITIES: set[str] = {
5766
"name",
@@ -250,7 +259,7 @@ def classify_columns(
250259

251260
llm_start = timer()
252261
response = client.chat.completions.create(
253-
model=DefaultLLMConfig.CONFIG_ID,
262+
model=DefaultLLMConfig.config_id(),
254263
messages=[
255264
{"role": "system", "content": DefaultLLMConfig.SYSTEM_PROMPT},
256265
{"role": "user", "content": formatted_prompt},
@@ -576,7 +585,7 @@ def get_entity_extractor(
576585
extractor._model = GLiNER.from_pretrained(
577586
clsfy_cfg.gliner_model,
578587
map_location=map_location,
579-
local_files_only=env_flag_is_true("NSS_LOCAL_FILES_ONLY"),
588+
local_files_only=hf_offline_enabled(),
580589
)
581590
entity_types = DEFAULT_ENTITIES
582591
if clsfy_cfg.ner_entities:

src/nemo_safe_synthesizer/preflight/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
DatasetSizeCheck,
2020
GroupbyColumnCheck,
2121
HFModelAvailabilityCheck,
22-
InferenceKeyCheck,
22+
InferenceModelCheck,
2323
OrderbyColumnCheck,
2424
OversamplingCheck,
2525
PseudoColumnCheck,
@@ -61,7 +61,7 @@
6161
"DatasetSizeCheck",
6262
"GroupbyColumnCheck",
6363
"HFModelAvailabilityCheck",
64-
"InferenceKeyCheck",
64+
"InferenceModelCheck",
6565
"IssueCollector",
6666
"MetadataCheck",
6767
"MetadataView",

src/nemo_safe_synthesizer/preflight/checks/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from .environment import (
2828
CUDAAvailabilityCheck,
2929
HFModelAvailabilityCheck,
30-
InferenceKeyCheck,
30+
InferenceModelCheck,
3131
VRAMHeadroomCheck,
3232
)
3333
from .metadata import TokenBudgetCheck
@@ -39,7 +39,7 @@
3939
"DatasetSizeCheck",
4040
"GroupbyColumnCheck",
4141
"HFModelAvailabilityCheck",
42-
"InferenceKeyCheck",
42+
"InferenceModelCheck",
4343
"OrderbyColumnCheck",
4444
"OversamplingCheck",
4545
"PseudoColumnCheck",
@@ -57,7 +57,7 @@
5757
_CORE_CHECKS: tuple[PreflightCheck, ...] = (
5858
# CONFIG
5959
CUDAAvailabilityCheck(),
60-
InferenceKeyCheck(),
60+
InferenceModelCheck(),
6161
HFModelAvailabilityCheck(),
6262
# DATAFRAME
6363
DatasetSizeCheck(),

0 commit comments

Comments
 (0)