diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 120fc034b4..a5a83d7239 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -11,7 +11,7 @@ Run these from the repo root (they wrap `cd docs/fern && npm run …`): | `make docs-deps` | Install docs tooling (first run on a machine) | | `make docs` | Local dev server (live preview) | | `make docs-watch` | Local dev server plus repo-level watcher for `docs/**` changes outside `docs/fern/` | -| `make docs-check` | `fern check` + MDX validation + gated-link check (what CI runs) | +| `make docs-check` | `fern check` + MDX validation + NotebookViewer artifact validation + gated-link check (what CI runs) | | `make docs-check-python-snippets DOCS_PATH=...` | Syntax-check and type-check Python fenced snippets in one doc | | `make docs-run-notebook DOCS_PATH=...` | Execute the source notebook for one Fern `.mdx`/`.ipynb` doc using `nemo-nb` markers | | `make docs-broken-links` | Report broken links | @@ -24,7 +24,7 @@ Use `make docs` when you are only editing `docs/fern/` config. Use `make docs-wa ## Rules that bite if you miss them - **Navigation is the build.** Fern only builds pages listed in `docs/fern/versions/latest.yml`. A `.mdx` not in the nav is **not built** (404, not indexed) — that is how unready features are gated. Do **not** use `hidden: true` for gating (it still builds/serves the page). -- **Gated (unready) features** stay in the repo but out of the nav: `auth/`, `customizer/`, `safe-synthesizer/`, `evaluator/benchmarks/`, and a few individual pages. Ready-to-paste nav blocks for re-publishing are in `docs/fern/gated-nav.yml`. To publish one: move its block into `latest.yml`, re-add inbound links, run `make docs-check && make docs-broken-links`. +- **Publication state is nav-derived.** Do not maintain or rely on a hard-coded list of gated directories. Check `docs/fern/versions/latest.yml`: listed pages are published, and omitted pages are gated. `docs/fern/gated-nav.yml` contains reference blocks for some gated features. To publish one: move its block into `latest.yml`, re-add inbound links, run `make docs-check && make docs-broken-links`. - **Don't link into gated pages.** A link from a published page into a gated page is a dead link. `make docs-check` fails on it; `make docs-fix-links` delinks it to plain text. (Replaces the old MkDocs `hide_unready_docs` auto-delinking.) - **Internal links** use canonical nav URLs like `/documentation/get-started/core-concepts/workspaces`, not relative `.md`/source paths. `make docs-broken-links` is the check. - **No `{{variable}}` substitutions.** Fern has no substitution step; product names are inlined as literal text. (Prompt-template tokens like `` `{{input}}` `` inside backticks are real content — leave them.) diff --git a/docs/customizer/about.mdx b/docs/customizer/about.mdx index b7cf983f24..c9c3cec9a2 100644 --- a/docs/customizer/about.mdx +++ b/docs/customizer/about.mdx @@ -2,6 +2,7 @@ title: "Customization Concepts" description: "" --- + This page provides an overview of the customization concepts for the NeMo Platform. @@ -14,30 +15,30 @@ Supervised fine-tuning (SFT) is a traditional technique for customizing a pre-tr Full SFT models require a NIM deployment to serve inference. The Deployment Management Service supports two deployment modes: -| Deployment Mode | Image Type | Weight Loading | Best For | -|-----------------|------------|----------------|----------| -| **Multi-LLM** (Default) | Generic multi-model NIM | On-the-fly download via Files service | Any HF model, custom fine-tuned models, development | -| **Model-Specific NIM** | Dedicated model image | Pre-download via model puller | Production, optimized performance and latency | +| Deployment Mode | Image Type | Weight Loading | Best For | +| ----------------------- | ----------------------- | ------------------------------------- | --------------------------------------------------------------------------- | +| **Multi-LLM** (Default) | Generic multi-model NIM | On-the-fly download via Files service | Supported Hugging Face architectures, custom fine-tuned models, development | +| **Model-Specific NIM** | Dedicated model image | Pre-download via model puller | Production, optimized performance and latency | -- **Multi-LLM Image**: Can deploy any HuggingFace-compatible model, providing maximum flexibility for custom fine-tuned models. Does not guarantee optimized inference performance. +- **Multi-LLM Image**: Can deploy Hugging Face checkpoints whose architectures are supported by the image's inference engine, providing flexibility for custom fine-tuned models. Importing a checkpoint does not guarantee training or deployment compatibility; for example, Automodel LoRA does not support Conv1D-based architectures. It also does not guarantee optimized inference performance. - **Model-Specific NIM**: Provides optimized inference performance and latency through model-specific optimizations. Recommended for production deployments where performance is critical. ## Parameter-Efficient Fine-Tuning -Parameter-Efficient Fine-Tuning (PEFT) methods enable efficient model customization by training a small number of parameters while keeping the base model frozen. For example, when customizing LLaMa 3.3 70B: +Parameter-Efficient Fine-Tuning (PEFT) methods enable efficient model customization by training a small number of parameters while keeping the base model frozen. For example, when customizing Llama 3.3 70B: -- **Traditional SFT**: Trains and stores ~40 GB per task. -- **PEFT**: Trains and stores only a few MB per task while maintaining comparable performance. +- **Traditional SFT**: Produces a full BF16 checkpoint of approximately 140 GB per task. During training, budget free disk space separately for the base checkpoint, intermediate checkpoint, and final output—approximately 3× the downloaded base checkpoint size. +- **PEFT**: Produces an adapter that is approximately 100–500 MB per task while maintaining comparable performance. During training, budget approximately 1.5× the downloaded base checkpoint size. ```mermaid --- caption: Traditional Fine-Tuning --- flowchart TD - T1[Task 1] --> M1[LLaMa 3.3 - 70B] - T2[Task 2] --> M2[LLaMa 3.3 - 70B] - T3[Task 3] --> M3[LLaMa 3.3 - 70B] + T1[Task 1] --> M1[Llama 3.3 - 70B] + T2[Task 2] --> M2[Llama 3.3 - 70B] + T3[Task 3] --> M3[Llama 3.3 - 70B] style M1 fill:#B8D5F2 style M2 fill:#B8D5F2 @@ -47,13 +48,13 @@ flowchart TD ```mermaid --- -caption: Parameter-Efficient Fine Tuning +caption: Parameter-Efficient Fine-Tuning --- flowchart TD P1[Task 1] --> A1[LoRA 113M] P2[Task 2] --> A2[LoRA 20M] P3[Task 3] --> A3[LoRA 10M] - A1 & A2 & A3 --> M[LLaMa 3.3 - 70B] + A1 & A2 & A3 --> M[Llama 3.3 - 70B] style A1 fill:#B8D5F2 style A2 fill:#B8D5F2 @@ -121,6 +122,7 @@ Additional Resources: ## Training with Your Own Data Use NeMo Customizer to train custom models on your own data. The workflow can be carried out as follows: + - Upload a dataset - Train a custom model - Perform inference with the trained model @@ -205,17 +207,17 @@ Hyperparameters are configuration settings used to control the training process. Common hyperparameters you'll tune include: -| Hyperparameter | Description | -|----------------|-------------| -| Epochs | Number of complete passes through the training dataset | -| Batch size | Number of samples processed before updating model weights | -| Learning rate | Step size for weight updates during training | -| LoRA rank | Low-rank dimension of the adapter (lower = fewer parameters, higher = more expressive) | -| LoRA alpha | LoRA scaling factor | +| Hyperparameter | Description | +| -------------- | -------------------------------------------------------------------------------------- | +| Epochs | Number of complete passes through the training dataset | +| Batch size | Number of samples processed before updating model weights | +| Learning rate | Step size for weight updates during training | +| LoRA rank | Low-rank dimension of the adapter (lower = fewer parameters, higher = more expressive) | +| LoRA alpha | LoRA scaling factor | -NeMo Customizer offers **two training backends** — Automodel (multi-GPU) and Unsloth (single-GPU, quantized) — and each accepts its own job configuration. The exact field names, defaults, and available knobs differ between them. For the full per-backend hyperparameter reference, see [Training Configuration](/documentation/customizer-reference/manage-customization-jobs/training-configuration). +NeMo Customizer offers **two training backends** — Automodel (multi-GPU) and Unsloth (single-GPU, with optional quantized loading for LoRA) — and each accepts its own job configuration. Unsloth full-weight training requires unquantized model loading. The exact field names, defaults, and available knobs differ between them. For the full per-backend hyperparameter reference, see [Training Configuration](/documentation/customizer-reference/manage-customization-jobs/training-configuration). @@ -234,24 +236,31 @@ TP can be configured via `parallelism.tensor_parallel_size` in the [training con As of release 25.10.0, AutoModel engines including Phi-4, Qwen, and Gemma support tensor parallelism greater than 1 through the multi-GPU LoRA patch. Previous releases only supported `TP=1` for these models. + +#### Tensor Parallelism Configuration + +**Constraints** + +- TP must be less than or equal to the total number of GPUs available. +- TP should divide the total GPU count evenly. + +**Multi-node considerations** + +TP can span nodes, but doing so increases network communication overhead. For multi-node setups, keep TP within a single node when possible. High-bandwidth inter-node connections such as InfiniBand are important when TP must span nodes. + +For example, with 2 nodes and 4 GPUs per node, start with `TP=4` to keep tensor-parallel operations within each node. If the model still requires more memory, increase to `TP=8` to distribute tensor operations across both nodes. + +**Performance** + +- Smaller TP values generally have less communication overhead. +- Larger TP values provide more memory savings but increase communication costs. + ### Pipeline Parallelism [Pipeline Parallelism](https://docs.nvidia.com/nemo-framework/user-guide/latest/nemotoolkit/features/parallelisms.html#pipeline-parallelism) (PP) distributes the layers of a neural network across GPUs. The GPUs then process the different layers sequentially. PP can be configured via `parallelism.pipeline_parallel_size` in the [training configuration](/documentation/customizer-reference/manage-customization-jobs/training-configuration). -#### Configuration - -- Constraints - - TP must be less than or equal to the total number of GPUs available. It should be a factor of the total GPU count (divisible evenly). -- Multi-node considerations - - TP can span across nodes, but this introduces network communication overhead. For multi-node setups, it's often recommended to keep TP within a single node when possible. If using TP across nodes, high-bandwidth inter-node connections (like InfiniBand) become critical. - - Example: if you have 2 nodes with 4 GPUs each, start with TP=4 first. This keeps all tensor parallel operations within a single node. If your model still uses too much GPU memory with this setting, increase to TP=8, which will distribute tensor operations across both nodes. -- Performance - - Smaller TP values generally have less communication overhead. - - Larger TP values provide more memory savings but increase communication costs. - ### Context Parallelism [Context Parallelism](https://docs.nvidia.com/nemo-framework/user-guide/latest/nemotoolkit/features/parallelisms.html#context-parallelism) (CP) distributes activation memory along the sequence dimension across GPUs, which is particularly useful when training on datasets with very long sequences. @@ -287,3 +296,10 @@ If sequence packing is enabled for a model that does not support it, fine-tuning Learn how to create a LoRA customization job with sequence packing by following the [Optimizing for Tokens/GPU](/documentation/customizer-reference/tutorials/optimize-throughput) tutorial. + +## Next Steps + +- Review all backend-specific options in [Training Configuration](/documentation/customizer-reference/manage-customization-jobs/training-configuration). +- [Create a LoRA customization job](/documentation/customizer-reference/tutorials/lora-customization-job). +- [Create a Full SFT customization job](/documentation/customizer-reference/tutorials/sft-customization-job). +- [Optimize training throughput](/documentation/customizer-reference/tutorials/optimize-throughput) with sequence packing. diff --git a/docs/customizer/index.mdx b/docs/customizer/index.mdx index 488229a01d..4b405ec48b 100644 --- a/docs/customizer/index.mdx +++ b/docs/customizer/index.mdx @@ -68,7 +68,7 @@ View the available Phi models from Microsoft, designed for strong reasoning capa -View the available GPT-OSS models supported for Full SFT customization. +View the available GPT-OSS models supported for Full SFT and LoRA customization. @@ -118,21 +118,21 @@ Learn how to format datasets for different model types. datasets chat-models completion-models - + Learn how to start a LoRA customization job using a custom dataset. nemo-customizer - + Learn how to start a SFT customization job using a custom dataset. nemo-customizer - + Learn how to compress a larger teacher model into a smaller student model. @@ -146,7 +146,7 @@ Learn how to check job metrics using MLFlow or Weights & Biases. nemo-customizer mlflow wandb - + Learn how to optimize the token-per-GPU throughput for a LoRA optimization job. diff --git a/docs/customizer/manage-customization-jobs/create-job.mdx b/docs/customizer/manage-customization-jobs/create-job.mdx index 90d3940d7d..441cf038ee 100644 --- a/docs/customizer/manage-customization-jobs/create-job.mdx +++ b/docs/customizer/manage-customization-jobs/create-job.mdx @@ -9,7 +9,7 @@ Customization jobs are submitted to one of two backends. Choose the backend that | Backend | Best for | Methods | |---------|----------|---------| | **Automodel** (default) | Production fine-tuning, larger models, multi-GPU scaling | SFT, distillation; LoRA, merged-LoRA, or full-weight | -| **Unsloth** | Memory-constrained single-GPU LoRA | SFT; LoRA or full-weight, with 4-bit / 8-bit loading | +| **Unsloth** | Memory-constrained single-GPU training | SFT; LoRA with optional 4-bit / 8-bit loading, or unquantized full-weight | ## Prerequisites @@ -19,7 +19,7 @@ Before you can create a customization job, make sure that you have: - Created a [FileSet and Model Entity](/documentation/customizer-reference/manage-model-entities/overview) for your base model. - [Uploaded a dataset](/documentation/get-started/core-concepts/manage-files) as a FileSet. - Determined the [training configuration](/documentation/customizer-reference/manage-customization-jobs/training-configuration) you want to use for the customization job. -- Verified that the platform has sufficient storage for the job. Full SFT jobs require approximately 3× the base model size in free disk space; LoRA jobs require approximately 1.5×. See [ft-tut-understand-models](/documentation/customizer-reference/tutorials/understanding-models-and-training) for details. If you are also deploying the model from a base checkpoint fileset, plan for ~2.5× model size overall for LoRA. +- Verified that the platform has sufficient storage for the job. Budget against the downloaded base checkpoint size: Full SFT jobs require approximately 3× in free disk space, and LoRA jobs require approximately 1.5×. See [ft-tut-understand-models](/documentation/customizer-reference/tutorials/understanding-models-and-training) for details. Include any retained deployment copies separately. - Set the `NMP_BASE_URL` environment variable to your NeMo Platform endpoint. ```bash @@ -67,13 +67,15 @@ print(f"Submitted job: {job.job.name}") print(f"Job status: {job.job.status}") ``` +The response preserves the explicit `name`. If you omit `name`, the platform generates a backend-prefixed job name. + :open: ```json { - "name": "automodel-a1b2c3d4e5f6", + "name": "my-lora-job", "workspace": "default", "id": "platform-job-2k8i3i1HqJHHPVB5M6Bk9Z", "status": "queued", @@ -104,7 +106,7 @@ print(f"Job status: {job.job.status}") ## Submit an Unsloth Job -The Unsloth backend runs on a single GPU and supports 4-bit / 8-bit quantized loading. Build a `UnslothJobInput` spec and submit it to the `unsloth` backend. Note that Unsloth uses its own field names (`model.name`, `dataset.path`, `batch.per_device_train_batch_size`). +The Unsloth backend runs on a single GPU and supports 4-bit / 8-bit quantized loading for LoRA. Full-weight training requires `model.load_in_4bit=false` and `model.load_in_8bit=false`. Build a `UnslothJobInput` spec and submit it to the `unsloth` backend. Note that Unsloth uses its own field names (`model.name`, `dataset.path`, `batch.per_device_train_batch_size`). ```python import os diff --git a/docs/customizer/manage-customization-jobs/get-job-status.mdx b/docs/customizer/manage-customization-jobs/get-job-status.mdx index 0ca20c7e42..b7f852f887 100644 --- a/docs/customizer/manage-customization-jobs/get-job-status.mdx +++ b/docs/customizer/manage-customization-jobs/get-job-status.mdx @@ -10,7 +10,7 @@ Get detailed execution status for a customization job, including step-by-step pr This endpoint provides granular execution details including: - **Step-level status**: `model-and-dataset-download` → `training` → `model-upload` → `model-entity-creation` -- **Training metrics**: `step`, `epoch`, `loss`, `lr` (learning rate), `grad_norm`, `val_loss` +- **Training metrics**: `step`, `epoch`, `train_loss`, `lr` (learning rate), `grad_norm`, `val_loss` - **Progress tracking**: `downloaded_files`, `uploaded_bytes`, `progress_pct` To list jobs or get job definitions (model entity, hyperparameters, spec), use [List Active Jobs](/documentation/customizer-reference/manage-customization-jobs/list-active-jobs) instead. @@ -146,7 +146,7 @@ curl -X GET \ "num_epochs": 2, "step": 8, "epoch": 1, - "loss": 2.8918895721435547, + "train_loss": 2.8918895721435547, "lr": 4.9101714686276044e-05, "grad_norm": 26.0 } @@ -222,7 +222,7 @@ curl -X GET \ "num_epochs": 2, "step": 94, "epoch": 2, - "loss": 0.3437718152999878, + "train_loss": 0.3437718152999878, "lr": 5.000000000000001e-07, "grad_norm": 20.125, "val_loss": 0.5527229905128479, diff --git a/docs/customizer/manage-customization-jobs/hyperparameters.mdx b/docs/customizer/manage-customization-jobs/hyperparameters.mdx index 760d6493bf..d969f05a95 100644 --- a/docs/customizer/manage-customization-jobs/hyperparameters.mdx +++ b/docs/customizer/manage-customization-jobs/hyperparameters.mdx @@ -14,7 +14,7 @@ NeMo Customizer ships **two training backends**, and each accepts its own job co | Backend | Best for | Training methods | Hardware | |---------|----------|------------------|----------| | **Automodel** (default) | Production fine-tuning, larger models, multi-GPU scaling | SFT, distillation; LoRA, merged-LoRA, or full-weight | Single- or multi-GPU (tensor / pipeline / context / expert parallel) | -| **Unsloth** | Memory-constrained single-GPU LoRA | SFT; LoRA or full-weight | Single GPU (4-bit / 8-bit quantization) | +| **Unsloth** | Memory-constrained single-GPU training | SFT; LoRA or full-weight | Single GPU; optional 4-bit / 8-bit loading for LoRA, unquantized loading for full-weight | @@ -93,7 +93,7 @@ The `parallelism` block scales Automodel training across GPUs and nodes. | `parallelism.tensor_parallel_size` | GPUs for tensor parallelism (splits layers across GPUs for large models) | `1` | | `parallelism.pipeline_parallel_size` | GPUs for pipeline parallelism (splits model stages across GPUs) | `1` | | `parallelism.context_parallel_size` | GPUs for context parallelism (for very long sequences) | `1` | -| `parallelism.expert_parallel_size` | Expert parallelism for MoE models; must divide the number of experts | `null` | +| `parallelism.expert_parallel_size` | Expert parallelism for MoE models; must divide the number of experts. Leave unset for non-MoE models | `null` | @@ -102,7 +102,7 @@ The `parallelism` block scales Automodel training across GPUs and nodes. - `total_gpus = num_gpus_per_node × num_nodes`. - `total_gpus` must be divisible by `tensor_parallel_size × pipeline_parallel_size × context_parallel_size`. - `data_parallel_size` is derived as `total_gpus / (TP × PP × CP)`, and `global_batch_size` must be divisible by `micro_batch_size × data_parallel_size`. -- For MoE models, tensor parallelism must be `1` when `expert_parallel_size > 1`. +- For MoE models, when `expert_parallel_size` is set: the number of experts must be divisible by `expert_parallel_size`, `(data_parallel_size × context_parallel_size)` must be divisible by `expert_parallel_size`, and `tensor_parallel_size` must be `1` when `expert_parallel_size > 1`. @@ -131,7 +131,7 @@ When `training.training_type` is `"distillation"`, the following additional fiel ## Unsloth Configuration -An Unsloth job is configured with the following top-level sections: `model`, `dataset`, `training`, `schedule`, `batch`, `optimizer`, `hardware`, `output`, and (optionally) `integrations`. Unsloth runs on a **single GPU** and supports 4-bit / 8-bit quantized loading. +An Unsloth job is configured with the following top-level sections: `model`, `dataset`, `training`, `schedule`, `batch`, `optimizer`, `hardware`, `output`, and (optionally) `integrations`. Unsloth runs on a **single GPU** and supports 4-bit / 8-bit quantized loading for LoRA. Full-weight training must load the model without quantization. ### Model @@ -143,6 +143,8 @@ An Unsloth job is configured with the following top-level sections: `model`, `da | `model.load_in_8bit` | Load the base model in 8-bit | `false` | | `model.dtype` | Compute dtype (`auto`, `bfloat16`, `float16`, `float32`) | `auto` | | `model.trust_remote_code` | Allow custom model code from the checkpoint | `false` | +| `model.device_map` | Device placement forwarded to Unsloth. Accepts `auto`, `balanced`, `sequential`, a device index, or a custom map. `null` pins the model to the single visible GPU | `null` | +| `model.rope_scaling` | RoPE scaling configuration for long-context extension, such as `{"type": "linear", "factor": 2.0}` | `null` | @@ -166,7 +168,7 @@ Full-weight training (`training.finetuning_type: "all_weights"`) cannot be combi |-----------|--------|-------------|---------| | `training.training_type` | `sft` | Training method | `sft` | | `training.finetuning_type` | `lora`, `all_weights` | Adapter regime. `lora` trains an adapter; `all_weights` performs full-weight training | `lora` | -| `training.lora` | `{ rank, alpha, dropout, target_modules, bias, use_rslora, random_state }` | LoRA configuration (auto-filled with defaults when `finetuning_type` is `lora`) | *(see below)* | +| `training.lora` | `LoRAParams` object | LoRA configuration (auto-filled with defaults when `finetuning_type` is `lora`) | *(see below)* | | `training.use_gradient_checkpointing` | `unsloth`, `true`, `false` | Gradient checkpointing mode. `unsloth` uses Unsloth's optimized implementation | `unsloth` | LoRA parameters (`training.lora`): @@ -180,6 +182,12 @@ LoRA parameters (`training.lora`): | `bias` | Bias training mode (`none`, `all`, `lora_only`) | `none` | | `use_rslora` | Use rank-stabilized LoRA | `false` | | `random_state` | LoRA initialization seed | `3407` | +| `use_dora` | Use weight-decomposed LoRA (DoRA). Can improve quality at low ranks with additional training overhead | `false` | +| `loftq_config` | LoftQ initialization configuration for quantized base models | `null` | +| `modules_to_save` | Additional non-LoRA modules to train and save in full, such as `embed_tokens` or `lm_head` | `null` | +| `layers_to_transform` | Layer index or list of layer indexes to receive LoRA; `null` applies LoRA to all layers | `null` | +| `layer_replication` | Layer-replication ranges, such as `[[0, 16], [8, 24]]` | `null` | +| `init_lora_weights` | LoRA initialization: `true`, `false`, `gaussian`, `pissa`, `olora`, or `loftq` | `true` | ### Schedule @@ -194,6 +202,7 @@ LoRA parameters (`training.lora`): | `schedule.save_steps` | Checkpoint cadence (steps) | `null` | | `schedule.eval_steps` | Evaluation cadence (steps) | `null` | | `schedule.seed` | Random seed | `3407` | +| `schedule.lr_scheduler_kwargs` | Additional scheduler arguments, such as `{"num_cycles": 3}` for `cosine_with_restarts` | `null` | ### Batch @@ -209,6 +218,12 @@ LoRA parameters (`training.lora`): | `optimizer.learning_rate` | Step size for weight updates | `2e-4` | | `optimizer.weight_decay` | L2 regularization strength | `0.0` | | `optimizer.optim` | Optimizer (`adamw_torch`, `adamw_torch_fused`, `adamw_8bit`, `paged_adamw_8bit`, `sgd`). 8-bit optimizers reduce optimizer-state memory | `adamw_8bit` | +| `optimizer.adam_beta1` | Adam/AdamW first-moment decay | `0.9` | +| `optimizer.adam_beta2` | Adam/AdamW second-moment decay | `0.999` | +| `optimizer.adam_epsilon` | Adam/AdamW epsilon for numerical stability | `1e-8` | +| `optimizer.max_grad_norm` | Maximum gradient norm for clipping | `1.0` | +| `optimizer.label_smoothing_factor` | Cross-entropy label smoothing factor; `0.0` disables smoothing | `0.0` | +| `optimizer.neftune_noise_alpha` | NEFTune embedding-noise alpha; `null` disables NEFTune | `null` | ### Hardware @@ -217,9 +232,15 @@ LoRA parameters (`training.lora`): | `hardware.gpus` | Comma-separated GPU indices (`0` or `0,1`) for `CUDA_VISIBLE_DEVICES` (selection, not reservation) | `null` | | `hardware.precision` | Mixed-precision dtype (`bf16`, `fp16`). `bf16` recommended for Ampere+ | `bf16` | -### Output (save method) +### Output -Unsloth's output `save_method` controls the saved checkpoint shape: +| Parameter | Description | Default | +|-----------|-------------|---------| +| `output.name` | Output Model Entity or adapter name | Auto-generated from the job name | +| `output.description` | Optional description for the generated artifact | `null` | +| `output.save_method` | LoRA checkpoint serialization (see below); omit for full-weight training | `lora` | + +The `output.save_method` field accepts: | `save_method` | Result | |---------------|--------| @@ -227,7 +248,7 @@ Unsloth's output `save_method` controls the saved checkpoint shape: | `merged_16bit` | Merges the adapter into the base and saves a 16-bit checkpoint | | `merged_4bit` | Merges the adapter into the base and saves a 4-bit checkpoint | -The `merged_*` methods are only valid when `training.finetuning_type` is `lora`. +The `merged_*` methods are only valid when `training.finetuning_type` is `lora`. When `training.finetuning_type` is `all_weights`, omit `output.save_method`; the training driver saves the full trained checkpoint. --- diff --git a/docs/customizer/manage-customization-jobs/index.mdx b/docs/customizer/manage-customization-jobs/index.mdx index f01cfe9349..88d8fe9619 100644 --- a/docs/customizer/manage-customization-jobs/index.mdx +++ b/docs/customizer/manage-customization-jobs/index.mdx @@ -7,7 +7,7 @@ Use customization jobs to fine-tune a [model](/documentation/customizer-referenc ## How It Works -A customization job references a **Model Entity** that contains the base model checkpoint, and is submitted to one of two backends — **automodel** (default, multi-GPU) or **unsloth** (single-GPU, quantized). The job then runs on the platform's GPU cluster. When training completes: +A customization job references a **Model Entity** that contains the base model checkpoint, and is submitted to one of two backends — **automodel** (default, multi-GPU) or **unsloth** (single-GPU, with optional quantized loading for LoRA). The job then runs on the platform's GPU cluster. When training completes: - **LoRA jobs**: Create an **Adapter** attached to the original Model Entity. Adapters can be auto-deployed to NIMs. - **Full fine-tuning jobs**: Create a **new Model Entity** with the customized weights, linked to the base model. diff --git a/docs/customizer/manage-customization-jobs/list-active-jobs.mdx b/docs/customizer/manage-customization-jobs/list-active-jobs.mdx index ef8ddb3603..e993f1cf80 100644 --- a/docs/customizer/manage-customization-jobs/list-active-jobs.mdx +++ b/docs/customizer/manage-customization-jobs/list-active-jobs.mdx @@ -3,7 +3,7 @@ title: "List Active Jobs" description: "" --- -List customization jobs and their high-level status. Customization jobs run on the platform's Jobs service, so you list them through that service and filter by `source` to scope the results to a backend (`automodel` or `unsloth`). Each entry includes the job definition (model, dataset, training configuration) and overall status. +List active customization jobs and their high-level status. Customization jobs run on the platform's Jobs service, so you list them through that service and filter by `source` and `status`. The `source` scopes results to a backend (`automodel` or `unsloth`), while `status="active"` excludes completed, failed, and cancelled jobs. Each entry includes the job definition (model, dataset, training configuration) and overall status. @@ -25,7 +25,7 @@ export NMP_BASE_URL="https://your-nmp-base-url" ## To List Active Customization Jobs -Use the SDK to list jobs, filtering by `source` to scope the results to a customization backend: +Use the SDK to list jobs, filtering by `source` to scope the results to a customization backend and by `status` to return only active jobs: ```python import os @@ -37,10 +37,13 @@ client = NeMoPlatform( workspace="default", ) -# List automodel customization jobs +# List active automodel customization jobs jobs = client.jobs.list( workspace="default", - filter={"source": "automodel"}, # Use "unsloth" for the Unsloth backend + filter={ + "source": "automodel", # Use "unsloth" for the Unsloth backend + "status": "active", + }, page=1, page_size=10, sort="created_at", @@ -56,7 +59,8 @@ filtered_jobs = client.jobs.list( workspace="default", filter={ "source": "automodel", - "status": "active", # Filter by job status + "status": "active", + "project": "my-finetuning-project", }, sort="-created_at", # Sort by created_at descending ) @@ -118,7 +122,10 @@ for job in filtered_jobs.data: "total_results": 1 }, "sort": "created_at", - "filter": {}, + "filter": { + "source": "automodel", + "status": "active" + }, "search": {} } ``` diff --git a/docs/customizer/manage-model-entities/create-fileset.mdx b/docs/customizer/manage-model-entities/create-fileset.mdx index 410cea2757..2d629b9ad0 100644 --- a/docs/customizer/manage-model-entities/create-fileset.mdx +++ b/docs/customizer/manage-model-entities/create-fileset.mdx @@ -8,7 +8,7 @@ Create a FileSet containing your base model checkpoint before creating a Model E ## Prerequisites - Obtained the base URL of your NeMo Platform. -- For gated or private HuggingFace models: Created a secret with your HF token. Refer to [Manage Secrets](/documentation/get-started/core-concepts/manage-secrets). +- For gated or private Hugging Face models: Created a secret with your HF token. Refer to [Manage Secrets](/documentation/get-started/core-concepts/manage-secrets). - Set the `NMP_BASE_URL` environment variable. ```bash @@ -17,9 +17,9 @@ export NMP_BASE_URL="https://your-nemo-platform-url" --- -## From HuggingFace Hub +## From Hugging Face Hub -The most common method is downloading directly from HuggingFace. The example below uses [Qwen/Qwen3-1.7B](https://huggingface.co/Qwen/Qwen3-1.7B), a public model that requires no token: +The most common method is downloading directly from Hugging Face. The example below uses [Qwen/Qwen3-1.7B](https://huggingface.co/Qwen/Qwen3-1.7B), a public model that requires no token: ```python import os @@ -34,12 +34,12 @@ client = NeMoPlatform( HF_REPO_ID = "Qwen/Qwen3-1.7B" MODEL_NAME = "qwen3-1.7b" -# Create FileSet from HuggingFace +# Create FileSet from Hugging Face try: fileset = client.files.filesets.create( workspace="default", name=MODEL_NAME, - description="Qwen3 1.7B from HuggingFace", + description="Qwen3 1.7B from Hugging Face", purpose="model", storage=HuggingfaceStorageConfigParam( type="huggingface", @@ -57,10 +57,10 @@ print(f"FileSet ready: {fileset.name}") -Gated models (such as Llama) require a HuggingFace token. To use one: +Gated models (such as Llama) require a Hugging Face token. To use one: -1. Accept the model license on the HuggingFace model page. -2. Create a HuggingFace token with read access. +1. Accept the model license on the Hugging Face model page. +2. Create a Hugging Face token with read access. 3. Store the token as a secret in the platform (see [Manage Secrets](/documentation/get-started/core-concepts/manage-secrets)), then pass it as `token_secret` in the storage config: ```python @@ -90,7 +90,7 @@ client = NeMoPlatform( ) MODEL_NAME = "nemotron-mini-4b" -NGC_RESOURCE = "nemotron-mini-4b-instruct" +NGC_TARGET = "nemotron-mini-4b-instruct" NGC_ORG = "nvidia" NGC_TEAM = "nemo" NGC_VERSION = "1.0" @@ -122,7 +122,8 @@ try: type="ngc", org=NGC_ORG, team=NGC_TEAM, - resource=NGC_RESOURCE, # NGC resource name + target=NGC_TARGET, # NGC asset name + target_type="resource", version=NGC_VERSION, api_key_secret=ngc_secret.name, ), @@ -141,10 +142,11 @@ Files are downloaded in the background after you create a FileSet. Check the sta ```python # List files in the FileSet -files = client.files.list( +response = client.files.list( workspace="default", fileset="qwen3-1.7b", ) +files = response.data print(f"Files in FileSet ({len(files)} total):") for f in files[:10]: # Show first 10 diff --git a/docs/customizer/manage-model-entities/create-model-entity.mdx b/docs/customizer/manage-model-entities/create-model-entity.mdx index d06767e59a..a08bad2a64 100644 --- a/docs/customizer/manage-model-entities/create-model-entity.mdx +++ b/docs/customizer/manage-model-entities/create-model-entity.mdx @@ -96,9 +96,9 @@ print(f" Attention Heads: {model.spec.num_attention_heads}") "hidden_size": 2048, "num_layers": 28, "num_attention_heads": 16, - "num_key_value_heads": 8, + "num_kv_heads": 8, "vocab_size": 151936, - "max_sequence_length": 40960 + "context_size": 40960 } } ``` diff --git a/docs/customizer/manage-model-entities/index.mdx b/docs/customizer/manage-model-entities/index.mdx index 3fbe6ec2cd..0a747b3585 100644 --- a/docs/customizer/manage-model-entities/index.mdx +++ b/docs/customizer/manage-model-entities/index.mdx @@ -11,7 +11,7 @@ Before running a customization job, you need to set up a **Model Entity** that p -Create a FileSet containing your base model checkpoint from HuggingFace, NGC, or local storage. +Create a FileSet containing your base model checkpoint from Hugging Face, NGC, or local storage. @@ -35,7 +35,7 @@ A **FileSet** is a collection of files managed by the platform. For customizatio - Tokenizer files (`tokenizer.json`, `tokenizer_config.json`, and so on) FileSets can be populated from: -- **HuggingFace Hub** - Download directly from HF repositories +- **Hugging Face Hub** - Download directly from HF repositories - **NGC** - Download from NVIDIA NGC catalogs - **Local upload** - Upload files from your local machine @@ -56,7 +56,7 @@ Complete example of setting up a model for customization: -**HuggingFace Token**: If downloading from a gated HuggingFace repository (like Llama models), you will need to create a secret containing your HuggingFace API token first. Refer to [Manage Secrets](/documentation/get-started/core-concepts/manage-secrets) for instructions. +**Hugging Face Token**: If downloading from a gated Hugging Face repository (like Llama models), you will need to create a secret containing your Hugging Face API token first. Refer to [Manage Secrets](/documentation/get-started/core-concepts/manage-secrets) for instructions. ```python @@ -71,12 +71,12 @@ client = NeMoPlatform( workspace="default", ) -# Step 1: Create FileSet from HuggingFace +# Step 1: Create FileSet from Hugging Face try: fileset = client.files.filesets.create( workspace="default", name="qwen3-1.7b", - description="Qwen3 1.7B base model from HuggingFace", + description="Qwen3 1.7B base model from Hugging Face", storage=HuggingfaceStorageConfigParam( type="huggingface", repo_id="Qwen/Qwen3-1.7B", diff --git a/docs/customizer/models/data-format.mdx b/docs/customizer/models/data-format.mdx index dd0b3aba2b..5140cac824 100644 --- a/docs/customizer/models/data-format.mdx +++ b/docs/customizer/models/data-format.mdx @@ -9,7 +9,7 @@ Use the following guidelines to prepare your training dataset for the supported ## Dataset Preparation Guidelines - **File Format**: Save your training data as `.jsonl` files (one JSON object per line). -- **Validation**: Each record is automatically validated against the appropriate schema when training begins. The required format depends on `training.type` (for example, `sft`) specified in your job configuration. +- **Validation**: Each record is automatically validated against the appropriate schema when training begins. The required format depends on `training.training_type` (for example, `sft`) specified in your job configuration. For dataset creation tutorials, refer to [Format Training Dataset](/documentation/customizer-reference/tutorials/format-training-dataset). @@ -51,20 +51,27 @@ Each line in your JSONL file must contain a JSON object with these required fiel Each line in your JSONL file must contain a JSON object with these required fields: - **`messages`** (array of objects): The messages in the conversation. - - **`role`** (string): The role of the message. - - **`content`** (string): The content of the message. + - **`role`** (string): The role of the message. + - **`content`** (string): The content of the message. #### Example Dataset Entry -``` +```json { - "messages": [ - { - "role": "system", - "content": "You are an email writing assistant. Please help people write cogent emails." - }, - ... - ] + "messages": [ + { + "role": "system", + "content": "You are an email writing assistant. Please help people write cogent emails." + }, + { + "role": "user", + "content": "Write a concise follow-up after a project review." + }, + { + "role": "assistant", + "content": "Thank you for reviewing the project. Please let me know if you have any additional feedback." + } + ] } ``` @@ -158,10 +165,10 @@ Each line in your JSONL file must contain a JSON object with these required fiel #### Example Dataset Entry -``` +```json { - "prompt": "your string", - "completion": "your expected response" + "prompt": "What is the capital of France?", + "completion": "Paris." } ``` @@ -173,17 +180,23 @@ Each line in your JSONL file must contain a JSON object with these required fiel - **`system`** (string): The system message that defines the assistant's role or behavior. - **`conversations`** (array of objects): The conversation turns between user and assistant. - - **`from`** (string): The role of the message sender ("User" or "Assistant"). - - **`value`** (string): The content of the message. + - **`from`** (string): The role of the message sender (`User` or `Assistant`). + - **`value`** (string): The content of the message. #### Example Dataset Entry -``` +```json { - "system": "you are a robot", - "conversations": [ - {"from": "User", "value": "Choose a number that is greater than 0 and less than 2\n"}, - {"from": "Assistant", "value": "1"} - ] + "system": "You are a helpful assistant.", + "conversations": [ + { + "from": "User", + "value": "Choose a number that is greater than 0 and less than 2." + }, + { + "from": "Assistant", + "value": "1" + } + ] } ``` diff --git a/docs/customizer/models/embedding.mdx b/docs/customizer/models/embedding.mdx index 14ea7c522d..4abe495c94 100644 --- a/docs/customizer/models/embedding.mdx +++ b/docs/customizer/models/embedding.mdx @@ -20,7 +20,7 @@ This page provides detailed technical specifications for the embedding model fam | Training Data | Semi-supervised pre-training on 12M samples and fine-tuning on 1M samples from public QA datasets with commercial licenses | | License | [NVIDIA Open Model License](https://www.nvidia.com/en-us/agreements/enterprise-software/nvidia-open-model-license/), [Llama 3.2 Community License](https://www.llama.com/llama3_2/license/) | | Default Name | nvidia/llama-nemotron-embed-1b-v2 | -| HuggingFace | [nvidia/llama-nemotron-embed-1b-v2](https://huggingface.co/nvidia/llama-nemotron-embed-1b-v2) | +| Hugging Face | [nvidia/llama-nemotron-embed-1b-v2](https://huggingface.co/nvidia/llama-nemotron-embed-1b-v2) | | NIM | [nvidia/llama-nemotron-embed-1b-v2](https://catalog.ngc.nvidia.com/orgs/nim/teams/nvidia/containers/llama-nemotron-embed-1b-v2) | ### Model Entity Configuration @@ -59,9 +59,9 @@ The following table provides conservative hyperparameter defaults specifically o | Parameter | API Field Name | Type | Description | Recommended Value | | --- | --- | --- | --- | --- | -| Learning Rate | `learning_rate` | `number` | Step size for updating model parameters. Lower values help prevent overfitting in embedding models. | `5e-6` | -| Weight Decay | `weight_decay` | `number` | Regularization parameter to prevent overfitting by penalizing large weights. | `0.01` | -| Number of Epochs | `epochs` | `integer` | Number of complete passes through the training dataset. Limited to prevent overfitting. | `1` | +| Learning Rate | `optimizer.learning_rate` | `number` | Step size for updating model parameters. Lower values help prevent overfitting in embedding models. | `5e-6` | +| Weight Decay | `optimizer.weight_decay` | `number` | Regularization parameter to prevent overfitting by penalizing large weights. | `0.01` | +| Number of Epochs | `schedule.epochs` | `integer` | Number of complete passes through the training dataset. Limited to prevent overfitting. | `1` | | Training Data Size | N/A | N/A | Number of training examples to prevent overfitting while maintaining model performance. | `5,000-10,000` examples | NVIDIA recommends evaluating fine-tuned embedding models against the baseline to detect overfitting and potential performance degradation. @@ -79,8 +79,9 @@ This model supports inference deployment through NVIDIA Inference Microservices 1. **Deploy the model**: Create a ModelDeploymentConfig and ModelDeployment to deploy your fine-tuned model. See [about](/documentation/models-and-inference) for details. 2. **Access through Inference Gateway**: The Inference Gateway provides unified access to all deployed models via three routing patterns: - - **Model Entity routing**: `/v2/workspaces/{workspace}/inference/gateway/model/{name}/-/v1/embeddings` - - **Provider routing**: `/v2/workspaces/{workspace}/inference/gateway/provider/{deployment}/-/v1/embeddings` + - **Model Entity routing**: `/apis/inference-gateway/v2/workspaces/{workspace}/model/{name}/-/v1/embeddings` + - **Provider routing**: `/apis/inference-gateway/v2/workspaces/{workspace}/provider/{deployment}/-/v1/embeddings` + - **OpenAI routing**: `/apis/inference-gateway/v2/workspaces/{workspace}/openai/-/v1/embeddings` (specify the model in the request body) ```python import os @@ -132,6 +133,6 @@ embedding = response["data"][0]["embedding"] print(f"Embedding dimension: {len(embedding)}") ``` -For detailed fine-tuning instructions, refer to the [Embedding Customization tutorial](../tutorials/embedding-customization-job.ipynb). +For detailed fine-tuning instructions, refer to the [Embedding Customization tutorial](/documentation/customizer-reference/tutorials/embedding-customization-job). For more information about formatting training datasets for the embedding model, refer to [Dataset Format Requirements](/documentation/customizer-reference/models/dataset-format). diff --git a/docs/customizer/models/gpt-oss.mdx b/docs/customizer/models/gpt-oss.mdx index 2d1b4e2eb6..4012f0d3c3 100644 --- a/docs/customizer/models/gpt-oss.mdx +++ b/docs/customizer/models/gpt-oss.mdx @@ -8,7 +8,7 @@ This page provides detailed technical specifications for the OpenAI GPT-OSS mode ## Before You Start -These models require a HuggingFace token to download. Create a secret with your HuggingFace API key, then create a FileSet and Model Entity referencing the model. See [index](/documentation/customizer-reference/manage-model-entities/overview) for setup instructions. +These models require a Hugging Face token to download. Create a secret with your Hugging Face API key, then create a FileSet and Model Entity referencing the model. See [index](/documentation/customizer-reference/manage-model-entities/overview) for setup instructions. --- @@ -19,18 +19,24 @@ These models require a HuggingFace token to download. Create a secret with your | Creator | OpenAI | | Architecture | Mixture of Experts (MoE) Transformer | | Description | GPT-OSS 20B provides lower latency for local or specialized use cases, featuring full chain-of-thought reasoning and agentic capabilities. | -| Max I/O Tokens | Not specified | +| Context Length | 131,072 tokens | | Parameters | 21B parameters (3.6B active parameters) | | Training Data | Trained on harmony response format | -| Memory Requirements | Runs within 32GB of memory with BFloat16 quantization | +| Checkpoint Quantization | MXFP4 quantization of the MoE weights | +| Inference Memory | The official checkpoint can run within 16 GB of memory; Customizer training requires the GPU configurations below | | Default Name | openai/gpt-oss-20b | -| HuggingFace | [openai/gpt-oss-20b](https://huggingface.co/openai/gpt-oss-20b) | +| Hugging Face | [openai/gpt-oss-20b](https://huggingface.co/openai/gpt-oss-20b) | ### Training Options (20B) -- **LoRA**: 4x 80GB GPU, tensor parallel size 1, expert parallel size 4, pipeline parallel size 1 -- **Full SFT**: 8x 80GB GPU, tensor parallel size 1, expert parallel size 8, pipeline parallel size 1 -- Sequence Packing: Not supported +The Automodel contract matrix validates these configurations: + +| Fine-Tuning | Dataset Format | GPUs | Sequence Packing | +| --- | --- | --- | --- | +| LoRA | Prompt-completion | 1 | Supported | +| Full SFT | Prompt-completion or chat | 8 | Not supported in the tested configurations | + +For the tested Full SFT configuration, tensor and pipeline parallel sizes are `1` and expert parallel size is `8`. The tested LoRA configuration uses one GPU without expert parallelism. Default training max sequence length: 4096. @@ -81,6 +87,6 @@ GPT-OSS models use the harmony response format and require this format for prope -Sequence packing is not supported for GPT-OSS models in NeMo Customizer. +For GPT-OSS 20B, sequence packing is supported by the tested Automodel LoRA configuration with prompt-completion data. It is not supported by the tested Full SFT or chat configurations. diff --git a/docs/customizer/models/index.mdx b/docs/customizer/models/index.mdx index 8ea46b6871..25e86ca846 100644 --- a/docs/customizer/models/index.mdx +++ b/docs/customizer/models/index.mdx @@ -14,7 +14,7 @@ For fine-tuning and deployment tutorials, see the [Tutorials](/documentation/cus ## Before You Start -If downloading models hosted on Hugging Face, create a secret with your HuggingFace API key, then create a FileSet and Model Entity referencing the model. See [index](/documentation/customizer-reference/manage-model-entities/overview) for setup instructions. +If downloading models hosted on Hugging Face, create a secret with your Hugging Face API key, then create a FileSet and Model Entity referencing the model. See [index](/documentation/customizer-reference/manage-model-entities/overview) for setup instructions. --- @@ -63,7 +63,7 @@ View the available Mistral models, including Mistral and Ministral variants for ## Tested Models -The following table lists models that NVIDIA tested and their available features. This is a list of *known-good* combinations, not a list of limits: NeMo Customizer can fine-tune many models and regimes beyond those listed, including additional Hugging Face checkpoints, other fine-tuning regimes (LoRA, merged-LoRA, full-weight, distillation), and either training backend (Automodel or Unsloth). Models and regimes outside this table may work but have not been formally validated. +The following table lists models that NVIDIA tested and their available features. This is a list of *known-good* combinations, not a list of limits: NeMo Customizer can fine-tune additional Hugging Face checkpoints and regimes when their architectures are supported by the selected training backend. Compatibility varies by architecture and fine-tuning method—for example, Automodel LoRA does not support Conv1D-based models. Models and regimes outside this table may work but have not been formally validated; test them before relying on them in production. For detailed technical specifications of each model such as architecture, parameters, and token limits, refer to the [model family](#model-families) pages. @@ -81,7 +81,7 @@ The following models support both chat and completion model training. | [nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16) | No | Full SFT, LoRA | No | Supported (only Full SFT) | Yes | | [nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16) | No | LoRA | No | Supported | Yes | | [microsoft/phi-4](https://huggingface.co/microsoft/phi-4) | No | Full SFT, LoRA | No | Supported | No | -| [openai/gpt-oss-20b](https://huggingface.co/openai/gpt-oss-20b) | Yes | Full SFT, LoRA | No | Supported | Yes | +| [openai/gpt-oss-20b](https://huggingface.co/openai/gpt-oss-20b) | Yes | Full SFT, LoRA | LoRA with prompt-completion data | Supported | Yes | | [Qwen/Qwen2.5-1.5B-Instruct](https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct) | No | Full SFT, LoRA | No| Supported | Yes | | [Qwen/Qwen3-0.6B](https://huggingface.co/Qwen/Qwen3-0.6B) | No | Full SFT, LoRA | No | Supported | Yes | | [mistralai/Mistral-7B-Instruct-v0.3](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.3) | No | Full SFT, LoRA | No | Supported | No | diff --git a/docs/customizer/models/llama-nemotron.mdx b/docs/customizer/models/llama-nemotron.mdx index 091b420d7c..ccf85a78d3 100644 --- a/docs/customizer/models/llama-nemotron.mdx +++ b/docs/customizer/models/llama-nemotron.mdx @@ -17,7 +17,7 @@ This page provides detailed technical specifications for the Nemotron model fami | Parameters | 8 billion | | Training Data | Not specified | | Default Name | nvidia/Llama-3.1-Nemotron-Nano-8B-v1 | -| HuggingFace | [nvidia/Llama-3.1-Nemotron-Nano-8B-v1](https://huggingface.co/nvidia/Llama-3.1-Nemotron-Nano-8B-v1) | +| Hugging Face | [nvidia/Llama-3.1-Nemotron-Nano-8B-v1](https://huggingface.co/nvidia/Llama-3.1-Nemotron-Nano-8B-v1) | | NIM | [nvidia/llama-3.1-nemotron-nano-8b-v1](https://catalog.ngc.nvidia.com/orgs/nim/teams/nvidia/containers/llama-3.1-nemotron-nano-8b-v1?version=1.8.4) | ### Training Options @@ -47,7 +47,7 @@ This page provides detailed technical specifications for the Nemotron model fami | Max I/O Tokens | 4096 | | Parameters | 9 billion | | Default Name | nvidia/NVIDIA-Nemotron-Nano-9B-v2 | -| HuggingFace | [nvidia/NVIDIA-Nemotron-Nano-9B-v2](https://huggingface.co/nvidia/NVIDIA-Nemotron-Nano-9B-v2) | +| Hugging Face | [nvidia/NVIDIA-Nemotron-Nano-9B-v2](https://huggingface.co/nvidia/NVIDIA-Nemotron-Nano-9B-v2) | | NIM | [NVIDIA-Nemotron-Nano-9B-v2](https://catalog.ngc.nvidia.com/orgs/nim/teams/nvidia/containers/nvidia-nemotron-nano-9b-v2?version=latest) | ### Training Options @@ -79,7 +79,7 @@ This page provides detailed technical specifications for the Nemotron model fami | MoE Configuration | 128 experts + 1 shared expert, 6 experts activated per token | | Supported Languages | English, German, Spanish, French, Italian, Japanese | | Default Name | nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16 | -| HuggingFace | [nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16) | +| Hugging Face | [nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16) | | NIM | [Nemotron-3-Nano-30B-A3B](https://catalog.ngc.nvidia.com/orgs/nim/teams/nvidia/containers/nemotron-3-nano?version=2.0.1) | ### Training Options @@ -115,7 +115,7 @@ Deployment for LoRA using NIM is not supported for this model. | Max I/O Tokens | 4096 | | Parameters | 120B total (12B active) | | Default Name | nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16 | -| HuggingFace | [nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16) | +| Hugging Face | [nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16](https://huggingface.co/nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-BF16) | ### Training Options diff --git a/docs/customizer/models/llama.mdx b/docs/customizer/models/llama.mdx index 31573aa778..a4fee46942 100644 --- a/docs/customizer/models/llama.mdx +++ b/docs/customizer/models/llama.mdx @@ -17,7 +17,7 @@ This page provides detailed technical specifications for the Llama model family | Parameters | 3 billion | | Training Data | 15+ trillion tokens (up to 2024) | | Default Name | meta-llama/Llama-3.2-3B-Instruct | -| HuggingFace | [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct) | +| Hugging Face | [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct) | ### Training Options @@ -46,7 +46,7 @@ This page provides detailed technical specifications for the Llama model family | Parameters | 1 billion | | Training Data | 15+ trillion tokens (up to 2024) | | Default Name | meta-llama/Llama-3.2-1B-Instruct | -| HuggingFace | [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) | +| Hugging Face | [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) | ### Training Options @@ -76,7 +76,7 @@ This page provides detailed technical specifications for the Llama model family | Parameters | 8 billion | | Training Data | 15 trillion tokens (up to December 2023) | | Default Name | meta-llama/Llama-3.1-8B-Instruct | -| HuggingFace | [meta-llama/Llama-3.1-8B-Instruct](https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct) | +| Hugging Face | [meta-llama/Llama-3.1-8B-Instruct](https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct) | ### Training Options diff --git a/docs/customizer/models/mistral.mdx b/docs/customizer/models/mistral.mdx index dc3850f9c7..9d6cbcb716 100644 --- a/docs/customizer/models/mistral.mdx +++ b/docs/customizer/models/mistral.mdx @@ -17,7 +17,7 @@ This page provides detailed technical specifications for the Mistral model famil | Parameters | 7 billion | | Training Data | Not specified | | Default Name | mistralai/Mistral-7B-Instruct-v0.3 | -| HuggingFace | [mistralai/Mistral-7B-Instruct-v0.3](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.3) | +| Hugging Face | [mistralai/Mistral-7B-Instruct-v0.3](https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.3) | ### Training Options @@ -46,7 +46,7 @@ This page provides detailed technical specifications for the Mistral model famil | Parameters | 3 billion | | Training Data | Not specified | | Default Name | mistralai/Ministral-3-3B-Instruct-2512 | -| HuggingFace | [mistralai/Ministral-3-3B-Instruct-2512](https://huggingface.co/mistralai/Ministral-3-3B-Instruct-2512) | +| Hugging Face | [mistralai/Ministral-3-3B-Instruct-2512](https://huggingface.co/mistralai/Ministral-3-3B-Instruct-2512) | ### Training Options @@ -69,7 +69,7 @@ Deployment using NIM is not supported for this model. | Parameters | 3 billion | | Training Data | Not specified | | Default Name | mistralai/Ministral-3-3B-Reasoning-2512 | -| HuggingFace | [mistralai/Ministral-3-3B-Reasoning-2512](https://huggingface.co/mistralai/Ministral-3-3B-Reasoning-2512) | +| Hugging Face | [mistralai/Ministral-3-3B-Reasoning-2512](https://huggingface.co/mistralai/Ministral-3-3B-Reasoning-2512) | ### Training Options diff --git a/docs/customizer/models/phi.mdx b/docs/customizer/models/phi.mdx index caeaf972a1..ad498c654e 100644 --- a/docs/customizer/models/phi.mdx +++ b/docs/customizer/models/phi.mdx @@ -16,7 +16,7 @@ This page provides detailed technical specifications for the Phi model family su | Parameters | 14 billion | | Training Data | High-quality data with emphasis on reasoning and code | | Default Name | microsoft/phi-4 | -| HuggingFace | [microsoft/phi-4](https://huggingface.co/microsoft/phi-4) | +| Hugging Face | [microsoft/phi-4](https://huggingface.co/microsoft/phi-4) | ### Training Options diff --git a/docs/customizer/models/qwen.mdx b/docs/customizer/models/qwen.mdx index 3d414a3d3d..473002a750 100644 --- a/docs/customizer/models/qwen.mdx +++ b/docs/customizer/models/qwen.mdx @@ -17,7 +17,7 @@ This page provides detailed technical specifications for the Qwen model family s | Parameters | 1.5 billion | | Training Data | Not specified | | Default Name | Qwen/Qwen2.5-1.5B-Instruct | -| HuggingFace | [Qwen/Qwen2.5-1.5B-Instruct](https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct) | +| Hugging Face | [Qwen/Qwen2.5-1.5B-Instruct](https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct) | ### Training Options @@ -46,7 +46,7 @@ This page provides detailed technical specifications for the Qwen model family s | Parameters | 0.6 billion | | Training Data | Not specified | | Default Name | Qwen/Qwen3-0.6B | -| HuggingFace | [Qwen/Qwen3-0.6B](https://huggingface.co/Qwen/Qwen3-0.6B) | +| Hugging Face | [Qwen/Qwen3-0.6B](https://huggingface.co/Qwen/Qwen3-0.6B) | ### Training Options diff --git a/docs/customizer/tutorials/_snippets/output/chat-basic-format-example.jsonl b/docs/customizer/tutorials/_snippets/output/chat-basic-format-example.jsonl deleted file mode 100644 index 3d63e84cd3..0000000000 --- a/docs/customizer/tutorials/_snippets/output/chat-basic-format-example.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"messages": [{"role": "system","content": ""}, {"role": "user","content": ""}, {"role": "assistant","content": ""}]} diff --git a/docs/customizer/tutorials/_snippets/output/chat-expanded-format-example.json b/docs/customizer/tutorials/_snippets/output/chat-expanded-format-example.json deleted file mode 100644 index c98ab1de5b..0000000000 --- a/docs/customizer/tutorials/_snippets/output/chat-expanded-format-example.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "messages": [ - { - "role": "system", - "content": "" - }, { - "role": "user", - "content": "" - }, { - "role": "assistant", - "content": "" - } - ] -} diff --git a/docs/customizer/tutorials/_snippets/output/chat-thinking-off-example.jsonl b/docs/customizer/tutorials/_snippets/output/chat-thinking-off-example.jsonl deleted file mode 100644 index 7cf16c2f8f..0000000000 --- a/docs/customizer/tutorials/_snippets/output/chat-thinking-off-example.jsonl +++ /dev/null @@ -1,5 +0,0 @@ -{"messages": [ - {"role": "system", "content": "detailed thinking off"}, - {"role": "user", "content": "What is 2 + 2?"}, - {"role": "assistant", "content": "4"} -]} diff --git a/docs/customizer/tutorials/_snippets/output/chat-thinking-on-example.jsonl b/docs/customizer/tutorials/_snippets/output/chat-thinking-on-example.jsonl deleted file mode 100644 index 637a0e338e..0000000000 --- a/docs/customizer/tutorials/_snippets/output/chat-thinking-on-example.jsonl +++ /dev/null @@ -1,5 +0,0 @@ -{"messages": [ - {"role": "system", "content": "detailed thinking on"}, - {"role": "user", "content": "What is 2 + 2?"}, - {"role": "assistant", "content": "To solve 2 + 2, add 2 and 2 together. The answer is 4."} -]} diff --git a/docs/customizer/tutorials/_snippets/output/chat-tool-calling-basic-example.jsonl b/docs/customizer/tutorials/_snippets/output/chat-tool-calling-basic-example.jsonl deleted file mode 100644 index 7a1e354929..0000000000 --- a/docs/customizer/tutorials/_snippets/output/chat-tool-calling-basic-example.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"messages": [{"role": "user","content": ""},{"role": "assistant","content": "","tool_calls": [{"type": "function","function": {"name": "fibonacci","arguments": {"n": 20}}}]}],"tools": [{"type": "function","function": {"name": "fibonacci","description": "Calculates the nth Fibonacci number.","parameters": {"type": "object","properties": {"n": {"description": "The position of the Fibonacci number.","type": "integer"}}}}}]} diff --git a/docs/customizer/tutorials/_snippets/output/chat-tool-calling-expanded-example.json b/docs/customizer/tutorials/_snippets/output/chat-tool-calling-expanded-example.json deleted file mode 100644 index 2bf7f0d13b..0000000000 --- a/docs/customizer/tutorials/_snippets/output/chat-tool-calling-expanded-example.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "messages": [ - { - "role": "user", - "content": "" - }, - { - "role": "assistant", - "content": "", - "tool_calls": [{ - "type": "function", - "function": { - "name": "fibonacci", - "arguments": {"n": 20} - } - }] - } - ], - "tools": [{ - "type": "function", - "function": { - "name": "fibonacci", - "description": "Calculates the nth Fibonacci number.", - "parameters": { - "type": "object", - "properties": { - "n": { - "description": "The position of the Fibonacci number.", - "type": "integer" - } - } - } - } - }] -} diff --git a/docs/customizer/tutorials/_snippets/output/completion-format-example.jsonl b/docs/customizer/tutorials/_snippets/output/completion-format-example.jsonl deleted file mode 100644 index 9fc99eeb16..0000000000 --- a/docs/customizer/tutorials/_snippets/output/completion-format-example.jsonl +++ /dev/null @@ -1 +0,0 @@ -{"prompt": "Hello", "completion": " world."} diff --git a/docs/customizer/tutorials/_snippets/output/config-list-example.json b/docs/customizer/tutorials/_snippets/output/config-list-example.json deleted file mode 100644 index 25bc371e54..0000000000 --- a/docs/customizer/tutorials/_snippets/output/config-list-example.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "object": "list", - "data": [ - { - "name": "meta/llama-3.2-1b-instruct@v1.0.0+80GB", - "namespace": "default", - "dataset_schemas": [ - { - "title": "Newline-Delimited JSON File", - "type": "array", - "items": { - "description": "Schema for Supervised Fine-Tuning (SFT) training data items.", - "properties": { - "prompt": { - "description": "The prompt for the entry", - "title": "Prompt", - "type": "string" - }, - "completion": { - "description": "The completion to train on", - "title": "Completion", - "type": "string" - } - }, - "required": ["prompt", "completion"], - "title": "SFTDatasetItemSchema", - "type": "object" - } - } - ], - "training_options": [ - { - "training_type": "sft", - "finetuning_type": "lora", - "num_gpus": 1, - "num_nodes": 1, - "tensor_parallel_size": 1, - "use_sequence_parallel": false - }, - { - "training_type": "sft", - "finetuning_type": "all_weights", - "num_gpus": 1, - "num_nodes": 1, - "tensor_parallel_size": 1, - "use_sequence_parallel": false - } - ] - }, - { - "name": "nvidia/llama-3.2-nv-embedqa-1b@v2+80GB", - "namespace": "nvidia", - "dataset_schemas": [ - { - "title": "Newline-Delimited JSON File", - "type": "array", - "items": { - "description": "Schema for embedding training data items.", - "properties": { - "query": { - "description": "The query to use as an anchor", - "title": "Query", - "type": "string" - }, - "pos_doc": { - "description": "A document that should match positively with the anchor", - "title": "Positive Document", - "type": "string" - }, - "neg_doc": { - "description": "Documents that should not match with the anchor", - "title": "Negative Documents", - "type": "array", - "items": {"type": "string"} - } - }, - "required": ["query", "pos_doc", "neg_doc"], - "title": "EmbeddingDatasetItemSchema", - "type": "object" - } - } - ], - "training_options": [ - { - "training_type": "sft", - "finetuning_type": "lora_merged", - "num_gpus": 1, - "num_nodes": 1, - "tensor_parallel_size": 1, - "use_sequence_parallel": false - } - ] - } - ] -} diff --git a/docs/customizer/tutorials/_snippets/output/job-creation-response.json b/docs/customizer/tutorials/_snippets/output/job-creation-response.json deleted file mode 100644 index 11c3c6741f..0000000000 --- a/docs/customizer/tutorials/_snippets/output/job-creation-response.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "id": "cust-Pi95UoDbNcqwgkruAB8LY6", - "created_at": "2025-02-19T20:10:06.278132", - "updated_at": "2025-02-19T20:10:06.278133", - "workspace": "default", - "spec": { - "model": "default/llama-3.2-1b-instruct", - "dataset": "fileset://default/test-dataset", - "training": { - "type": "sft", - "peft": { - "type": "lora", - "rank": 8, - "alpha": 32, - "dropout": 0.01 - }, - "batch_size": 16, - "epochs": 10, - "learning_rate": 0.0001 - }, - "output": {"name": "llama-3.2-1b-instruct-test-dataset-a1b2c3d4e5f6", "type": "adapter", "fileset": "llama-3.2-1b-instruct-test-dataset-a1b2c3d4e5f6"} - }, - "status": "created", - "custom_fields": {} -} diff --git a/docs/customizer/tutorials/_snippets/output/lora-hyperparameters-example.json b/docs/customizer/tutorials/_snippets/output/lora-hyperparameters-example.json deleted file mode 100644 index 114aa5c34d..0000000000 --- a/docs/customizer/tutorials/_snippets/output/lora-hyperparameters-example.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "training": { - "type": "sft", - "peft": { - "type": "lora", - "rank": 8, - "dropout": 0.01 - }, - "epochs": 10, - "batch_size": 16, - "learning_rate": 0.0001 - } -} diff --git a/docs/customizer/tutorials/_snippets/output/lora-training-data-example.jsonl b/docs/customizer/tutorials/_snippets/output/lora-training-data-example.jsonl deleted file mode 100644 index e0fb6a2dc9..0000000000 --- a/docs/customizer/tutorials/_snippets/output/lora-training-data-example.jsonl +++ /dev/null @@ -1,2 +0,0 @@ -{"prompt": "You are an email writting assistant, please help people write cogent emails.\n\nTell John I finally got the PS5 with 1TB from Target and will be home at 8 pm so we can play Call of Duty. State Jason told me it was fun.", "completion": "Dear John, I got it! I managed to find the new PS5 at Target. I will be home by 8pm so we can play Call of Duty on it. It actually is a lot of fun (or so has Jason told me). It is the 1tb one so I hope it serves us well. See you later. Jackson Lee"} -{"prompt": "You are an email writting assistant, please help people write cogent emails.\n\nCheck in with the horseback riding facility if my daughter can go for a trial ride to see if she likes it. Ask if the horses are gentle and what the capacity is for each lesson.","completion": "Hello! My 9-year-old is interested in horseback riding lessons. She's only ridden ponies at the beach, but had taken up a kean interest in horses lately. How many children are in each lesson group? Are the horses gentle? Would she be able to try a lesson before committing to a full six-month contract? Thank you for replying at your earliest convenience. Heather"} diff --git a/docs/customizer/tutorials/_snippets/output/version-error-example.json b/docs/customizer/tutorials/_snippets/output/version-error-example.json deleted file mode 100644 index 9dc8710d4f..0000000000 --- a/docs/customizer/tutorials/_snippets/output/version-error-example.json +++ /dev/null @@ -1 +0,0 @@ -{ "detail": "Version is not specified in the config URN: meta/llama-3.2-1b-instruct" } diff --git a/docs/customizer/tutorials/distillation-customization-job.ipynb b/docs/customizer/tutorials/distillation-customization-job.ipynb index 00d6515395..59b68cd619 100644 --- a/docs/customizer/tutorials/distillation-customization-job.ipynb +++ b/docs/customizer/tutorials/distillation-customization-job.ipynb @@ -1,930 +1,959 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "\n", - "\n", - "# Knowledge Distillation Customization\n", - "\n", - "Learn how to train a smaller student model to mimic a larger teacher model using knowledge distillation (KD).\n", - "\n", - "## About\n", - "\n", - "Knowledge Distillation transfers knowledge from a large **teacher** model to a smaller **student** model. During training, the student learns to match the teacher's output probability distribution, producing a compact model that retains much of the teacher's capability.\n", - "\n", - "**What you can achieve with KD:**\n", - "\n", - "- **Compress models:** Distill a 3B model into a 1B model for faster inference and lower deployment costs\n", - "- **Reduce latency:** Deploy a smaller model that responds faster while preserving quality\n", - "- **Lower resource requirements:** Serve a distilled model on fewer GPUs\n", - "\n", - "### KD vs SFT: Understanding the Trade-offs\n", - "\n", - "| Aspect | Full SFT | Knowledge Distillation |\n", - "| --- | --- | --- |\n", - "| **Training signal** | Ground-truth labels only | Teacher's soft probability distribution + labels |\n", - "| **Knowledge source** | Dataset examples | Teacher model's learned representations |\n", - "| **Output model size** | Same as input model | Typically a smaller student model |\n", - "| **GPU requirements** | Needs to fit one model | Needs to fit both teacher and student in memory |\n", - "| **Best for** | Domain adaptation, new knowledge injection | Model compression, latency reduction |\n", - "\n", - "### Key Parameters\n", - "\n", - "| Parameter | Default | Description |\n", - "| --- | --- | --- |\n", - "| `teacher_model` | *(required)* | Teacher model entity URN (e.g., `default/llama-3-2-3b-teacher`) |\n", - "| `teacher_precision` | `bf16` | Precision for the frozen teacher (`bf16`, `fp16`, `fp32`). Lower = less memory |\n", - "| `distillation_ratio` | `0.5` | Balance between CE loss and KD loss. `0.0` = CE only, `1.0` = KD only |\n", - "| `distillation_temperature` | `1.0` | Softmax temperature. Higher = softer distributions, more knowledge transfer |\n", - "\n", - "### Workflow Overview\n", - "\n", - "This tutorial follows a complete distillation pipeline:\n", - "\n", - "1. **Fine-tune the teacher** (SFT on the task dataset) so it learns the domain\n", - "2. **Establish a baseline** by deploying the base student model and measuring ROUGE scores\n", - "3. **Distill into the student** using the fine-tuned teacher's soft targets\n", - "4. **Evaluate the distilled student** and compare ROUGE scores against the baseline\n", - "\n", - "**When to choose KD:**\n", - "\n", - "- You have a high-quality large model and want a smaller, faster version\n", - "- Deployment latency or cost is a constraint\n", - "- The teacher and student share the same vocabulary (e.g., both are Llama models)\n", - "\n", - "**When to choose SFT instead:** Refer to the [Full SFT tutorial](./sft-customization-job) when you want to train a model directly on labeled data without a teacher." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Prerequisites\n", - "\n", - "Before starting this tutorial, ensure you have:\n", - "\n", - "1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n", - "2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n", - "3. **Installed evaluation dependencies:**\n", - "\n", - "```sh\n", - "pip install evaluate rouge_score datasets\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Quick Start\n", - "\n", - "### 1. Initialize SDK\n", - "\n", - "The SDK needs to know your NeMo Platform server URL. By default, `http://localhost:8080` is used in accordance with the [Quickstart](../../get-started/quickstart.md) guide. If NeMo Platform is running at a custom location, you can override the URL by setting the `NMP_BASE_URL` environment variable:\n", - "\n", - "```sh\n", - "export NMP_BASE_URL=\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import json\n", - "import os\n", - "import time\n", - "import uuid\n", - "from pathlib import Path\n", - "\n", - "from nemo_platform import NeMoPlatform, ConflictError\n", - "\n", - "NMP_BASE_URL = os.environ.get(\"NMP_BASE_URL\", \"http://localhost:8080\")\n", - "client = NeMoPlatform(\n", - " base_url=NMP_BASE_URL,\n", - " workspace=\"default\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2. Prepare Dataset\n", - "\n", - "Knowledge distillation uses the same dataset formats as SFT. We use the SQuAD dataset for both teacher training and distillation so that the teacher first learns the task, then transfers that knowledge to the student.\n", - "\n", - "We also hold out a small **test split** for ROUGE evaluation at the end." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from datasets import load_dataset, DatasetDict\n", - "\n", - "print(\"Loading dataset rajpurkar/squad\")\n", - "raw_dataset = load_dataset(\"rajpurkar/squad\")\n", - "if not isinstance(raw_dataset, DatasetDict):\n", - " raise ValueError(\"Dataset does not contain expected splits\")\n", - "\n", - "print(\"Loaded dataset\")\n", - "\n", - "SEED = 1234\n", - "TRAINING_SIZE = 3000\n", - "VALIDATION_SIZE = 300\n", - "TEST_SIZE = 100\n", - "DATASET_PATH = Path(\"kd-dataset\").absolute()\n", - "\n", - "os.makedirs(DATASET_PATH, exist_ok=True)\n", - "\n", - "train_set = raw_dataset.get('train')\n", - "split = train_set.train_test_split(test_size=0.05, seed=SEED)\n", - "\n", - "train_ds = split['train'].select(range(min(TRAINING_SIZE, len(split['train']))))\n", - "val_ds = split['test'].select(range(min(VALIDATION_SIZE, len(split['test']))))\n", - "test_ds = split['test'].select(range(VALIDATION_SIZE, min(VALIDATION_SIZE + TEST_SIZE, len(split['test']))))\n", - "\n", - "\n", - "def convert_squad(example):\n", - " \"\"\"Convert SQuAD format to prompt/completion format.\"\"\"\n", - " prompt = f\"Context: {example['context']} Question: {example['question']} Answer:\"\n", - " completion = example[\"answers\"][\"text\"][0]\n", - " return {\"prompt\": prompt, \"completion\": completion}\n", - "\n", - "\n", - "def write_jsonl(dataset, path):\n", - " with open(path, \"w\", encoding=\"utf-8\") as f:\n", - " for example in dataset:\n", - " f.write(json.dumps(convert_squad(example)) + \"\\n\")\n", - "\n", - "\n", - "def write_test_jsonl(dataset, path):\n", - " \"\"\"Save test split with raw context/question for chat-style evaluation.\"\"\"\n", - " with open(path, \"w\", encoding=\"utf-8\") as f:\n", - " for example in dataset:\n", - " f.write(json.dumps({\n", - " \"context\": example[\"context\"],\n", - " \"question\": example[\"question\"],\n", - " \"completion\": example[\"answers\"][\"text\"][0],\n", - " }) + \"\\n\")\n", - "\n", - "\n", - "write_jsonl(train_ds, f\"{DATASET_PATH}/training.jsonl\")\n", - "write_jsonl(val_ds, f\"{DATASET_PATH}/validation.jsonl\")\n", - "write_test_jsonl(test_ds, f\"{DATASET_PATH}/testing.jsonl\")\n", - "\n", - "print(f\"Training: {len(train_ds)} rows\")\n", - "print(f\"Validation: {len(val_ds)} rows\")\n", - "print(f\"Test: {len(test_ds)} rows\")\n", - "\n", - "with open(f\"{DATASET_PATH}/training.jsonl\", 'r') as f:\n", - " sample = json.loads(f.readline())\n", - " print(f\"\\nSample prompt: {sample['prompt'][:150]}...\")\n", - " print(f\"Sample completion: {sample['completion']}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "DATASET_NAME = \"kd-dataset\"\n", - "\n", - "try:\n", - " client.files.filesets.create(\n", - " workspace=\"default\",\n", - " name=DATASET_NAME,\n", - " description=\"Knowledge distillation training data\"\n", - " )\n", - " print(f\"Created fileset: {DATASET_NAME}\")\n", - "except ConflictError:\n", - " print(f\"Fileset '{DATASET_NAME}' already exists, continuing...\")\n", - "\n", - "client.files.upload(\n", - " local_path=f\"{DATASET_PATH}/\",\n", - " remote_path=\"\",\n", - " fileset=DATASET_NAME,\n", - " workspace=\"default\"\n", - ")\n", - "\n", - "print(\"Uploaded files:\")\n", - "print(json.dumps([f.model_dump() for f in client.files.list(fileset=DATASET_NAME, workspace=\"default\").data], indent=2))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3. Secrets Setup\n", - "\n", - "In this tutorial we use two Llama 3.2 Instruct models from HuggingFace:\n", - "- **Teacher:** [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct) (3B parameters)\n", - "- **Student:** [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) (1B parameters)\n", - "\n", - "Both models share the same tokenizer/vocabulary (required for knowledge distillation) and include a chat template for deployment with `/chat/completions`.\n", - "\n", - "**HuggingFace Authentication:**\n", - "- For gated models (Llama, Gemma), you must provide a HuggingFace token via the `token_secret` parameter\n", - "- Get your token from [HuggingFace Settings](https://huggingface.co/settings/tokens) (requires Read access)\n", - "- Accept the model's terms on the HuggingFace model page before using it:\n", - " - [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)\n", - " - [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "HF_TOKEN = os.getenv(\"HF_TOKEN\")\n", - "\n", - "\n", - "def create_or_get_secret(name: str, value: str | None, label: str):\n", - " if not value:\n", - " raise ValueError(f\"{label} is not set\")\n", - " try:\n", - " secret = client.secrets.create(\n", - " name=name,\n", - " workspace=\"default\",\n", - " value=value,\n", - " )\n", - " print(f\"Created secret: {name}\")\n", - " return secret\n", - " except ConflictError:\n", - " print(f\"Secret '{name}' already exists, continuing...\")\n", - " return client.secrets.retrieve(name=name, workspace=\"default\")\n", - "\n", - "\n", - "hf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\n", - "print(\"HF_TOKEN secret:\")\n", - "print(hf_secret.model_dump_json(indent=2))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 4. Create Model FileSets and Model Entities\n", - "\n", - "Knowledge distillation requires **two** model entities:\n", - "1. **Student model** — the smaller model that will be trained ([meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct))\n", - "2. **Teacher model** — the larger model that provides soft targets ([meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct))\n", - "\n", - "Using the Instruct variants ensures the output model includes a chat template, which is required for the `/chat/completions` inference endpoint." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from nemo_platform.types.files import HuggingfaceStorageConfigParam\n", - "\n", - "SPEC_TIMEOUT_SECONDS = 120\n", - "\n", - "\n", - "def create_model(hf_repo: str, model_name: str, description: str):\n", - " \"\"\"Create a fileset + model entity and wait for ModelSpec.\"\"\"\n", - " try:\n", - " client.files.filesets.create(\n", - " workspace=\"default\",\n", - " name=model_name,\n", - " description=description,\n", - " storage=HuggingfaceStorageConfigParam(\n", - " type=\"huggingface\",\n", - " repo_id=hf_repo,\n", - " repo_type=\"model\",\n", - " token_secret=hf_secret.name\n", - " )\n", - " )\n", - " print(f\"Created fileset: {model_name}\")\n", - " except ConflictError:\n", - " print(f\"Fileset '{model_name}' already exists.\")\n", - "\n", - " try:\n", - " model = client.models.create(\n", - " workspace=\"default\",\n", - " name=model_name,\n", - " fileset=f\"default/{model_name}\",\n", - " )\n", - " print(f\"Created Model Entity: {model_name}\")\n", - " except ConflictError:\n", - " print(f\"Model '{model_name}' already exists. Updating fileset.\")\n", - " model = client.models.update(\n", - " workspace=\"default\",\n", - " name=model_name,\n", - " fileset=f\"default/{model_name}\",\n", - " )\n", - "\n", - " print(f\"Waiting for ModelSpec on {model_name}...\")\n", - " spec_start = time.time()\n", - " while not model.spec:\n", - " if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n", - " raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS}s\")\n", - " time.sleep(2)\n", - " model = client.models.retrieve(workspace=\"default\", name=model_name)\n", - " print(f\"ModelSpec populated: {model.spec}\")\n", - " return model\n", - "\n", - "\n", - "student_model = create_model(\n", - " hf_repo=\"meta-llama/Llama-3.2-1B-Instruct\",\n", - " model_name=\"llama-3-2-1b-student\",\n", - " description=\"Llama 3.2 1B Instruct student model\",\n", - ")\n", - "\n", - "print()\n", - "\n", - "teacher_model = create_model(\n", - " hf_repo=\"meta-llama/Llama-3.2-3B-Instruct\",\n", - " model_name=\"llama-3-2-3b-teacher\",\n", - " description=\"Llama 3.2 3B Instruct teacher model\",\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "\n", - "## Phase 1: Fine-Tune the Teacher\n", - "\n", - "### 5. Train Teacher with Full SFT\n", - "\n", - "For best distillation results, fine-tune the teacher on the **same dataset** that will be used for distillation. This ensures the teacher has learned the task-specific knowledge that the student will inherit.\n", - "\n", - "We train the 3B Instruct model with Full SFT on the SQuAD dataset." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from nemo_automodel_plugin.schema import AutomodelJobInput\n", - "\n", - "job_suffix = uuid.uuid4().hex[:4]\n", - "\n", - "TEACHER_JOB_NAME = f\"teacher-sft-job-{job_suffix}\"\n", - "TEACHER_OUTPUT_NAME = f\"teacher-model-{job_suffix}\"\n", - "\n", - "teacher_spec = AutomodelJobInput(\n", - " model=f\"default/{teacher_model.name}\",\n", - " dataset={\"training\": f\"default/{DATASET_NAME}\"},\n", - " training={\n", - " \"training_type\": \"sft\",\n", - " \"finetuning_type\": \"all_weights\",\n", - " \"max_seq_length\": 2048,\n", - " },\n", - " schedule={\"epochs\": 1},\n", - " batch={\"global_batch_size\": 64, \"micro_batch_size\": 1},\n", - " optimizer={\"learning_rate\": 5e-5},\n", - " parallelism={\"num_gpus_per_node\": 1},\n", - " output={\"name\": TEACHER_OUTPUT_NAME},\n", - ")\n", - "\n", - "teacher_job = client.customization.automodel.jobs.create(\n", - " spec=teacher_spec, workspace=\"default\", name=TEACHER_JOB_NAME\n", - ")\n", - "\n", - "TRAINED_TEACHER_NAME = TEACHER_OUTPUT_NAME\n", - "print(f\"Teacher training job: {teacher_job.job.name}\")\n", - "print(f\"Output teacher model: {TRAINED_TEACHER_NAME}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from IPython.display import clear_output\n", - "\n", - "\n", - "def wait_for_job(job_name: str):\n", - " \"\"\"Poll job status until completion.\"\"\"\n", - " while True:\n", - " status = client.jobs.get_status(name=job_name, workspace=\"default\")\n", - " clear_output(wait=True)\n", - " print(f\"Job: {job_name}\")\n", - " print(f\"Status: {status.status}\")\n", - "\n", - " for job_step in status.steps or []:\n", - " if job_step.name == \"training\":\n", - " for task in job_step.tasks or []:\n", - " details = task.status_details or {}\n", - " step = details.get(\"step\")\n", - " max_steps = details.get(\"max_steps\")\n", - " if step is not None and max_steps is not None:\n", - " print(f\"Progress: Step {step}/{max_steps} ({step / max_steps * 100:.1f}%)\")\n", - " phase = details.get(\"phase\")\n", - " if phase:\n", - " print(f\"Phase: {phase}\")\n", - " break\n", - " break\n", - "\n", - " if status.status in (\"completed\", \"failed\", \"cancelled\", \"error\"):\n", - " print(f\"\\nJob finished: {status.status}\")\n", - " return status\n", - "\n", - " time.sleep(10)\n", - "\n", - "\n", - "teacher_status = wait_for_job(TEACHER_JOB_NAME)\n", - "assert teacher_status.status == \"completed\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "\n", - "## Phase 2: Establish Baseline (Base Student)\n", - "\n", - "### 6. Deploy the Base Student Model\n", - "\n", - "Before distillation, deploy the base student model (1B Instruct, without any fine-tuning) to establish a baseline ROUGE score. After distillation, we compare the distilled student against this baseline to measure improvement." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "baseline_suffix = uuid.uuid4().hex[:4]\n", - "BASELINE_DEPLOYMENT_CONFIG = f\"baseline-student-cfg-{baseline_suffix}\"\n", - "BASELINE_DEPLOYMENT_NAME = f\"baseline-student-{baseline_suffix}\"\n", - "\n", - "baseline_deployment_config = client.inference.deployment_configs.create(\n", - " workspace=\"default\",\n", - " name=BASELINE_DEPLOYMENT_CONFIG,\n", - " engine=\"vllm\",\n", - " model_spec={\n", - " \"model_namespace\": \"default\",\n", - " \"model_name\": student_model.name,\n", - " },\n", - " executor_config={\n", - " \"gpu\": 1,\n", - " \"image_name\": \"vllm/vllm-openai\",\n", - " \"image_tag\": \"v0.22.1\",\n", - " },\n", - ")\n", - "\n", - "baseline_deployment = client.inference.deployments.create(\n", - " workspace=\"default\",\n", - " name=BASELINE_DEPLOYMENT_NAME,\n", - " config=baseline_deployment_config.name\n", - ")\n", - "\n", - "print(f\"Baseline student deployment: {baseline_deployment.name}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def wait_for_deployment(deployment_name: str, timeout_minutes: int = 30):\n", - " \"\"\"Poll deployment until ready.\"\"\"\n", - " start = time.time()\n", - " timeout = timeout_minutes * 60\n", - " while True:\n", - " dep = client.inference.deployments.retrieve(name=deployment_name, workspace=\"default\")\n", - " elapsed = time.time() - start\n", - " clear_output(wait=True)\n", - " print(f\"Deployment: {deployment_name}\")\n", - " print(f\"Status: {dep.status}\")\n", - " print(f\"Elapsed: {int(elapsed // 60)}m {int(elapsed % 60)}s\")\n", - "\n", - " if dep.status == \"READY\":\n", - " print(\"\\nDeployment is ready!\")\n", - " return dep\n", - " if dep.status in (\"FAILED\", \"ERROR\", \"TERMINATED\", \"LOST\"):\n", - " print(f\"\\nDeployment failed: {dep.status}\")\n", - " return dep\n", - " if elapsed > timeout:\n", - " print(f\"\\nTimeout ({timeout_minutes}m). Check status manually.\")\n", - " return dep\n", - " time.sleep(15)\n", - "\n", - "\n", - "dep_status = wait_for_deployment(BASELINE_DEPLOYMENT_NAME)\n", - "assert dep_status.status == \"READY\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 7. Generate Baseline Predictions on Test Set" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "with open(f\"{DATASET_PATH}/testing.jsonl\", \"r\", encoding=\"utf-8\") as f:\n", - " test_data = [json.loads(line) for line in f]\n", - "\n", - "contexts = [row[\"context\"] for row in test_data]\n", - "questions = [row[\"question\"] for row in test_data]\n", - "reference_completions = [row[\"completion\"] for row in test_data]\n", - "\n", - "print(f\"Test samples: {len(contexts)}\")\n", - "print(f\"Sample context: {contexts[0]}\")\n", - "print(f\"Sample question: {questions[0]}\")\n", - "print(f\"Sample reference: {reference_completions[0]}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def generate_completions(\n", - " deployment_name: str,\n", - " output_model_name: str,\n", - " contexts: list[str],\n", - " questions: list[str],\n", - ") -> list[str]:\n", - " \"\"\"Generate completions for a list of context/question pairs using a deployed model.\"\"\"\n", - " completions = []\n", - " for context, question in zip(contexts, questions):\n", - " messages = [\n", - " {\n", - " \"role\": \"user\",\n", - " \"content\": f\"Based on the following context, answer the question.\\n\\nContext: {context}\\n\\nQuestion: {question}\",\n", - " }\n", - " ]\n", - " response = client.inference.gateway.provider.post(\n", - " \"v1/chat/completions\",\n", - " name=deployment_name,\n", - " workspace=\"default\",\n", - " body={\n", - " \"model\": f\"default/{output_model_name}\",\n", - " \"messages\": messages,\n", - " \"temperature\": 0,\n", - " \"max_tokens\": 128,\n", - " }\n", - " )\n", - " completions.append(response[\"choices\"][0][\"message\"][\"content\"])\n", - " return completions\n", - "\n", - "\n", - "print(\"Generating baseline (base student) predictions...\")\n", - "baseline_completions = generate_completions(BASELINE_DEPLOYMENT_NAME, student_model.name, contexts, questions)\n", - "print(f\"Generated {len(baseline_completions)} baseline predictions\")\n", - "print(f\"\\nSample baseline output: {baseline_completions[0]}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 8. Delete Baseline Deployment\n", - "\n", - "Delete the baseline student deployment to free GPU resources for the distillation training job and subsequent distilled model deployment." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "client.inference.deployments.delete(name=BASELINE_DEPLOYMENT_NAME, workspace=\"default\")\n", - "print(f\"Deleted baseline deployment: {BASELINE_DEPLOYMENT_NAME}\")\n", - "\n", - "if not client.models.wait_for_status(\n", - " deployment_name=BASELINE_DEPLOYMENT_NAME,\n", - " desired_status=\"DELETED\",\n", - " workspace=\"default\",\n", - " timeout=600,\n", - "):\n", - " raise TimeoutError(\n", - " f\"Deployment {BASELINE_DEPLOYMENT_NAME} was not deleted within timeout\"\n", - " )\n", - "\n", - "client.inference.deployment_configs.delete(name=BASELINE_DEPLOYMENT_CONFIG, workspace=\"default\")\n", - "print(f\"Deleted baseline deployment config: {BASELINE_DEPLOYMENT_CONFIG}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "\n", - "## Phase 3: Distill into Student\n", - "\n", - "### 9. Create Knowledge Distillation Job\n", - "\n", - "Now create a distillation job that trains the 1B student using the **fine-tuned** 3B teacher's output distribution. The `model` field specifies the student, and `teacher_model` references the trained teacher model entity from Phase 1." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**GPU Requirements:**\n", - "\n", - "KD requires loading both student and teacher models, so plan GPU memory accordingly:\n", - "- 1B student + 3B teacher: 1 GPU (24GB+ VRAM each)\n", - "- 3B student + 8B teacher: 4 GPUs\n", - "- 8B student + 70B teacher: 8+ GPUs\n", - "\n", - "Use `teacher_precision=\"bf16\"` (default) to reduce teacher memory footprint." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from nemo_automodel_plugin.schema import AutomodelJobInput\n", - "\n", - "KD_JOB_NAME = f\"my-kd-job-{job_suffix}\"\n", - "KD_OUTPUT_NAME = f\"kd-student-{job_suffix}\"\n", - "\n", - "kd_spec = AutomodelJobInput(\n", - " model=f\"default/{student_model.name}\",\n", - " dataset={\"training\": f\"default/{DATASET_NAME}\"},\n", - " training={\n", - " \"training_type\": \"distillation\",\n", - " \"finetuning_type\": \"all_weights\",\n", - " \"teacher_model\": f\"default/{TRAINED_TEACHER_NAME}\",\n", - " \"teacher_precision\": \"bf16\",\n", - " \"distillation_ratio\": 0.5,\n", - " \"distillation_temperature\": 2.0,\n", - " \"max_seq_length\": 2048,\n", - " },\n", - " schedule={\"epochs\": 1},\n", - " batch={\"global_batch_size\": 64, \"micro_batch_size\": 1},\n", - " optimizer={\"learning_rate\": 5e-5},\n", - " parallelism={\"num_gpus_per_node\": 1},\n", - " output={\"name\": KD_OUTPUT_NAME},\n", - ")\n", - "\n", - "kd_job = client.customization.automodel.jobs.create(\n", - " spec=kd_spec, workspace=\"default\", name=KD_JOB_NAME\n", - ")\n", - "\n", - "DISTILLED_STUDENT_NAME = KD_OUTPUT_NAME\n", - "print(f\"Distillation job: {kd_job.job.name}\")\n", - "print(f\"Output student model: {DISTILLED_STUDENT_NAME}\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 10. Track Distillation Progress" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "kd_status = wait_for_job(KD_JOB_NAME)\n", - "assert kd_status.status == \"completed\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "---\n", - "\n", - "## Phase 4: Evaluate the Distilled Student Model\n", - "\n", - "### 11. Deploy the Distilled Student Model\n", - "\n", - "The output model has the same architecture as the 1B student—only its weights have been updated via distillation. It requires just 1 GPU to deploy." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "deploy_suffix_2 = uuid.uuid4().hex[:4]\n", - "STUDENT_DEPLOYMENT_CONFIG = f\"kd-student-deploy-cfg-{deploy_suffix_2}\"\n", - "STUDENT_DEPLOYMENT_NAME = f\"kd-student-deploy-{deploy_suffix_2}\"\n", - "\n", - "student_deployment_config = client.inference.deployment_configs.create(\n", - " workspace=\"default\",\n", - " name=STUDENT_DEPLOYMENT_CONFIG,\n", - " engine=\"vllm\",\n", - " model_spec={\n", - " \"model_namespace\": \"default\",\n", - " \"model_name\": DISTILLED_STUDENT_NAME,\n", - " },\n", - " executor_config={\n", - " \"gpu\": 1,\n", - " \"image_name\": \"vllm/vllm-openai\",\n", - " \"image_tag\": \"v0.22.1\",\n", - " },\n", - ")\n", - "\n", - "student_deployment = client.inference.deployments.create(\n", - " workspace=\"default\",\n", - " name=STUDENT_DEPLOYMENT_NAME,\n", - " config=student_deployment_config.name\n", - ")\n", - "\n", - "print(f\"Student deployment: {student_deployment.name}\")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "wait_for_deployment(STUDENT_DEPLOYMENT_NAME)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 12. Generate Student Predictions on Test Set" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Generating distilled student predictions...\")\n", - "student_completions = generate_completions(STUDENT_DEPLOYMENT_NAME, DISTILLED_STUDENT_NAME, contexts, questions)\n", - "print(f\"Generated {len(student_completions)} student predictions\")\n", - "print(f\"\\nSample student output: {student_completions[0]}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 13. Compute ROUGE Scores\n", - "\n", - "Compare the base student (before distillation) and the distilled student against the ground-truth reference completions using ROUGE metrics." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import evaluate\n", - "\n", - "rouge = evaluate.load(\"rouge\")\n", - "\n", - "baseline_scores = rouge.compute(predictions=baseline_completions, references=reference_completions)\n", - "student_scores = rouge.compute(predictions=student_completions, references=reference_completions)\n", - "\n", - "metrics = list(baseline_scores.keys())\n", - "header = f\"{'Model':<35} \" + \" \".join(f\"{m:>10}\" for m in metrics)\n", - "separator = \"-\" * len(header)\n", - "\n", - "print(\"=\" * 60)\n", - "print(\"ROUGE SCORE COMPARISON\")\n", - "print(\"=\" * 60)\n", - "print(header)\n", - "print(separator)\n", - "print(f\"{'Base Student (1B, no training)':<35} \" + \" \".join(f\"{baseline_scores[m]:>10.4f}\" for m in metrics))\n", - "print(f\"{'Distilled Student (1B, KD)':<35} \" + \" \".join(f\"{student_scores[m]:>10.4f}\" for m in metrics))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "print(\"Sample predictions (first 3):\\n\")\n", - "for i in range(min(3, len(contexts))):\n", - " print(f\"--- Sample {i + 1} ---\")\n", - " print(f\"Question: {questions[i]}\")\n", - " print(f\"Reference: {reference_completions[i]}\")\n", - " print(f\"Baseline: {baseline_completions[i][:200]}\")\n", - " print(f\"Distilled: {student_completions[i][:200]}\")\n", - " print()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Interpreting ROUGE Scores:**\n", - "\n", - "| Metric | Measures |\n", - "|--------|----------|\n", - "| **ROUGE-1** | Unigram overlap between prediction and reference |\n", - "| **ROUGE-2** | Bigram overlap (captures phrase-level similarity) |\n", - "| **ROUGE-L** | Longest common subsequence (captures sentence structure) |\n", - "| **ROUGE-Lsum** | ROUGE-L computed over full summaries |\n", - "\n", - "**What to expect:**\n", - "- The base student (1B, no training) provides a lower bound since it has not seen the task data\n", - "- The distilled student (1B, KD) should significantly outperform the base student, demonstrating the knowledge transferred from the 3B teacher\n", - "- If the distilled student scores are not much higher than the baseline, try increasing `distillation_temperature`, adjusting `distillation_ratio`, or training for more epochs\n", - "\n", - "---\n", - "\n", - "## Hyperparameters\n", - "\n", - "For detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the [Hyperparameter Reference](../manage-customization-jobs/hyperparameters.md).\n", - "\n", - "---\n", - "\n", - "## Troubleshooting\n", - "\n", - "**Job fails during model download:**\n", - "- Verify authentication secrets are configured (refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md))\n", - "- For gated HuggingFace models (Llama, Gemma), accept the license on the model page\n", - "- Check both `model` (student) and `teacher_model` URNs are correct\n", - "- Ensure both model entities exist: `client.models.retrieve(name=..., workspace=\"default\")`\n", - "\n", - "**Job fails with OOM (Out of Memory) error:**\n", - "\n", - "KD loads both models, so OOM is more likely than with SFT:\n", - "1. **First try:** Use `teacher_precision=\"bf16\"` to reduce teacher memory\n", - "2. **Still OOM:** Reduce `micro_batch_size` to 1\n", - "3. **Still OOM:** Reduce `global_batch_size` and `max_seq_length`\n", - "4. **Last resort:** Increase `num_gpus_per_node`\n", - "\n", - "**No chat template / `/chat/completions` fails:**\n", - "- Use Instruct model variants (e.g., `Llama-3.2-1B-Instruct`) instead of base models (`Llama-3.2-1B`). Base models do not include a chat template in their tokenizer, so the output model will also lack one.\n", - "\n", - "**Distilled model quality is poor:**\n", - "- Increase `distillation_temperature` (try 2.0–5.0) to transfer more nuanced knowledge\n", - "- Adjust `distillation_ratio`—if dataset labels are high-quality, lower the ratio; if the teacher is strong, raise it\n", - "- Increase `epochs` or `max_steps` for more training\n", - "- Verify teacher and student share the same vocabulary\n", - "\n", - "**Vocabulary mismatch error:**\n", - "- Teacher and student must use the same tokenizer. Use models from the same family (e.g., Llama 3.2 1B Instruct + Llama 3.2 3B Instruct)\n", - "\n", - "**Deployment fails:**\n", - "- Verify output model exists: `client.models.retrieve(name=DISTILLED_STUDENT_NAME, workspace=\"default\")`\n", - "- Check deployment logs: `client.inference.deployments.get_logs(name=deployment.name, workspace=\"default\")`\n", - "- The distilled model has the same size as the student, so GPU requirements match the student model\n", - "\n", - "\n", - "## Next Steps\n", - "\n", - "- [Monitor training metrics](fine-tune-metrics) in detail\n", - "- [Evaluate your fine-tuned model](../../evaluator/index) using the Evaluator service\n", - "- Learn about [LoRA customization](./lora-customization-job) for resource-efficient fine-tuning\n", - "- Learn about [Full SFT](./sft-customization-job) for direct supervised fine-tuning" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.14" - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "# Knowledge Distillation Customization\n", + "\n", + "Learn how to train a smaller student model to mimic a larger teacher model using knowledge distillation (KD).\n", + "\n", + "## About\n", + "\n", + "Knowledge Distillation transfers knowledge from a large **teacher** model to a smaller **student** model. During training, the student learns to match the teacher's output probability distribution, producing a compact model that retains much of the teacher's capability.\n", + "\n", + "**What you can achieve with KD:**\n", + "\n", + "- **Compress models:** Distill a 3B model into a 1B model for faster inference and lower deployment costs\n", + "- **Reduce latency:** Deploy a smaller model that responds faster while preserving quality\n", + "- **Lower resource requirements:** Serve a distilled model on fewer GPUs\n", + "\n", + "### KD vs SFT: Understanding the Trade-offs\n", + "\n", + "| Aspect | Full SFT | Knowledge Distillation |\n", + "| --- | --- | --- |\n", + "| **Training signal** | Ground-truth labels only | Teacher's soft probability distribution + labels |\n", + "| **Knowledge source** | Dataset examples | Teacher model's learned representations |\n", + "| **Output model size** | Same as input model | Typically a smaller student model |\n", + "| **GPU requirements** | Needs to fit one model | Needs to fit both teacher and student in memory |\n", + "| **Best for** | Domain adaptation, new knowledge injection | Model compression, latency reduction |\n", + "\n", + "### Key Parameters\n", + "\n", + "| Parameter | Default | Description |\n", + "| --- | --- | --- |\n", + "| `teacher_model` | *(required)* | Teacher model entity URN (e.g., `default/llama-3-2-3b-teacher`) |\n", + "| `teacher_precision` | `bf16` | Precision for the frozen teacher (`bf16`, `fp16`, `fp32`). Lower = less memory |\n", + "| `distillation_ratio` | `0.5` | Balance between CE loss and KD loss. `0.0` = CE only, `1.0` = KD only |\n", + "| `distillation_temperature` | `1.0` | Softmax temperature. Higher = softer distributions, more knowledge transfer |\n", + "\n", + "### Workflow Overview\n", + "\n", + "This tutorial follows a complete distillation pipeline:\n", + "\n", + "1. **Fine-tune the teacher** (SFT on the task dataset) so it learns the domain\n", + "2. **Establish a baseline** by deploying the base student model and measuring ROUGE scores\n", + "3. **Distill into the student** using the fine-tuned teacher's soft targets\n", + "4. **Evaluate the distilled student** and compare ROUGE scores against the baseline\n", + "\n", + "**When to choose KD:**\n", + "\n", + "- You have a high-quality large model and want a smaller, faster version\n", + "- Deployment latency or cost is a constraint\n", + "- The teacher and student share the same vocabulary (e.g., both are Llama models)\n", + "\n", + "**When to choose SFT instead:** Refer to the [Full SFT tutorial](./sft-customization-job) when you want to train a model directly on labeled data without a teacher." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "Before starting this tutorial, ensure you have:\n", + "\n", + "1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n", + "2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n", + "3. **Installed evaluation dependencies:**\n", + "\n", + "```sh\n", + "pip install evaluate rouge_score datasets\n", + "```\n", + "\n", + "4. **At least one GPU with CUDA 13+**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Quick Start\n", + "\n", + "### 1. Initialize SDK\n", + "\n", + "The SDK needs to know your NeMo Platform server URL. By default, `http://localhost:8080` is used in accordance with the [Quickstart](../../get-started/quickstart.md) guide. If NeMo Platform is running at a custom location, you can override the URL by setting the `NMP_BASE_URL` environment variable:\n", + "\n", + "```sh\n", + "export NMP_BASE_URL=\n", + "```" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import json\n", + "import os\n", + "import time\n", + "import uuid\n", + "from pathlib import Path\n", + "\n", + "from nemo_platform import NeMoPlatform, ConflictError\n", + "\n", + "NMP_BASE_URL = os.environ.get(\"NMP_BASE_URL\", \"http://localhost:8080\")\n", + "client = NeMoPlatform(\n", + " base_url=NMP_BASE_URL,\n", + " workspace=\"default\"\n", + ")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Prepare Dataset\n", + "\n", + "Knowledge distillation uses the same dataset formats as SFT. We use the SQuAD dataset for both teacher training and distillation so that the teacher first learns the task, then transfers that knowledge to the student.\n", + "\n", + "We also hold out a small **test split** for ROUGE evaluation at the end." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from datasets import load_dataset, DatasetDict\n", + "\n", + "print(\"Loading dataset rajpurkar/squad\")\n", + "raw_dataset = load_dataset(\"rajpurkar/squad\")\n", + "if not isinstance(raw_dataset, DatasetDict):\n", + " raise ValueError(\"Dataset does not contain expected splits\")\n", + "\n", + "print(\"Loaded dataset\")\n", + "\n", + "SEED = 1234\n", + "TRAINING_SIZE = 3000\n", + "VALIDATION_SIZE = 300\n", + "TEST_SIZE = 100\n", + "DATASET_PATH = Path(\"kd-dataset\").absolute()\n", + "\n", + "os.makedirs(DATASET_PATH, exist_ok=True)\n", + "\n", + "train_set = raw_dataset.get('train')\n", + "split = train_set.train_test_split(test_size=0.05, seed=SEED)\n", + "\n", + "train_ds = split['train'].select(range(min(TRAINING_SIZE, len(split['train']))))\n", + "val_ds = split['test'].select(range(min(VALIDATION_SIZE, len(split['test']))))\n", + "test_ds = split['test'].select(range(VALIDATION_SIZE, min(VALIDATION_SIZE + TEST_SIZE, len(split['test']))))\n", + "\n", + "\n", + "def convert_squad(example):\n", + " \"\"\"Convert SQuAD format to prompt/completion format.\"\"\"\n", + " prompt = f\"Context: {example['context']} Question: {example['question']} Answer:\"\n", + " completion = example[\"answers\"][\"text\"][0]\n", + " return {\"prompt\": prompt, \"completion\": completion}\n", + "\n", + "\n", + "def write_jsonl(dataset, path):\n", + " with open(path, \"w\", encoding=\"utf-8\") as f:\n", + " for example in dataset:\n", + " f.write(json.dumps(convert_squad(example)) + \"\\n\")\n", + "\n", + "\n", + "def write_test_jsonl(dataset, path):\n", + " \"\"\"Save test split with raw context/question for chat-style evaluation.\"\"\"\n", + " with open(path, \"w\", encoding=\"utf-8\") as f:\n", + " for example in dataset:\n", + " f.write(json.dumps({\n", + " \"context\": example[\"context\"],\n", + " \"question\": example[\"question\"],\n", + " \"completion\": example[\"answers\"][\"text\"][0],\n", + " }) + \"\\n\")\n", + "\n", + "\n", + "write_jsonl(train_ds, f\"{DATASET_PATH}/training.jsonl\")\n", + "write_jsonl(val_ds, f\"{DATASET_PATH}/validation.jsonl\")\n", + "write_test_jsonl(test_ds, f\"{DATASET_PATH}/testing.jsonl\")\n", + "\n", + "print(f\"Training: {len(train_ds)} rows\")\n", + "print(f\"Validation: {len(val_ds)} rows\")\n", + "print(f\"Test: {len(test_ds)} rows\")\n", + "\n", + "with open(f\"{DATASET_PATH}/training.jsonl\", 'r') as f:\n", + " sample = json.loads(f.readline())\n", + " print(f\"\\nSample prompt: {sample['prompt'][:150]}...\")\n", + " print(f\"Sample completion: {sample['completion']}\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "DATASET_NAME = \"kd-dataset\"\n", + "\n", + "try:\n", + " client.files.filesets.create(\n", + " workspace=\"default\",\n", + " name=DATASET_NAME,\n", + " description=\"Knowledge distillation training data\"\n", + " )\n", + " print(f\"Created fileset: {DATASET_NAME}\")\n", + "except ConflictError:\n", + " print(f\"Fileset '{DATASET_NAME}' already exists, continuing...\")\n", + "\n", + "client.files.upload(\n", + " local_path=f\"{DATASET_PATH}/\",\n", + " remote_path=\"\",\n", + " fileset=DATASET_NAME,\n", + " workspace=\"default\"\n", + ")\n", + "\n", + "print(\"Uploaded files:\")\n", + "print(json.dumps([f.model_dump() for f in client.files.list(fileset=DATASET_NAME, workspace=\"default\").data], indent=2))" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Secrets Setup\n", + "\n", + "In this tutorial we use two Llama 3.2 Instruct models from Hugging Face:\n", + "- **Teacher:** [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct) (3B parameters)\n", + "- **Student:** [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) (1B parameters)\n", + "\n", + "Both models share the same tokenizer/vocabulary (required for knowledge distillation) and include a chat template for deployment with `/chat/completions`.\n", + "\n", + "**Hugging Face Authentication:**\n", + "- For gated models (Llama, Gemma), you must provide a Hugging Face token via the `token_secret` parameter\n", + "- Get your token from [Hugging Face Settings](https://huggingface.co/settings/tokens) (requires Read access)\n", + "- Accept the model's terms on the Hugging Face model page before using it:\n", + " - [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)\n", + " - [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct)" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "HF_TOKEN = os.getenv(\"HF_TOKEN\")\n", + "\n", + "\n", + "def create_or_get_secret(name: str, value: str | None, label: str):\n", + " if not value:\n", + " raise ValueError(f\"{label} is not set\")\n", + " try:\n", + " secret = client.secrets.create(\n", + " name=name,\n", + " workspace=\"default\",\n", + " value=value,\n", + " )\n", + " print(f\"Created secret: {name}\")\n", + " return secret\n", + " except ConflictError:\n", + " print(f\"Secret '{name}' already exists, continuing...\")\n", + " return client.secrets.retrieve(name=name, workspace=\"default\")\n", + "\n", + "\n", + "hf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\n", + "print(\"HF_TOKEN secret:\")\n", + "print(hf_secret.model_dump_json(indent=2))" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4. Create Model FileSets and Model Entities\n", + "\n", + "Knowledge distillation requires **two** model entities:\n", + "1. **Student model** — the smaller model that will be trained ([meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct))\n", + "2. **Teacher model** — the larger model that provides soft targets ([meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct))\n", + "\n", + "Using the Instruct variants ensures the output model includes a chat template, which is required for the `/chat/completions` inference endpoint." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from nemo_platform.types.files import HuggingfaceStorageConfigParam\n", + "\n", + "SPEC_TIMEOUT_SECONDS = 120\n", + "\n", + "\n", + "def create_model(hf_repo: str, model_name: str, description: str):\n", + " \"\"\"Create a fileset + model entity and wait for ModelSpec.\"\"\"\n", + " try:\n", + " client.files.filesets.create(\n", + " workspace=\"default\",\n", + " name=model_name,\n", + " description=description,\n", + " storage=HuggingfaceStorageConfigParam(\n", + " type=\"huggingface\",\n", + " repo_id=hf_repo,\n", + " repo_type=\"model\",\n", + " token_secret=hf_secret.name\n", + " )\n", + " )\n", + " print(f\"Created fileset: {model_name}\")\n", + " except ConflictError:\n", + " print(f\"Fileset '{model_name}' already exists.\")\n", + "\n", + " try:\n", + " model = client.models.create(\n", + " workspace=\"default\",\n", + " name=model_name,\n", + " fileset=f\"default/{model_name}\",\n", + " )\n", + " print(f\"Created Model Entity: {model_name}\")\n", + " except ConflictError:\n", + " print(f\"Model '{model_name}' already exists. Updating fileset.\")\n", + " model = client.models.update(\n", + " workspace=\"default\",\n", + " name=model_name,\n", + " fileset=f\"default/{model_name}\",\n", + " )\n", + "\n", + " print(f\"Waiting for ModelSpec on {model_name}...\")\n", + " spec_start = time.time()\n", + " while not model.spec:\n", + " if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n", + " raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS}s\")\n", + " time.sleep(2)\n", + " model = client.models.retrieve(workspace=\"default\", name=model_name)\n", + " print(f\"ModelSpec populated: {model.spec}\")\n", + " return model\n", + "\n", + "\n", + "student_model = create_model(\n", + " hf_repo=\"meta-llama/Llama-3.2-1B-Instruct\",\n", + " model_name=\"llama-3-2-1b-student\",\n", + " description=\"Llama 3.2 1B Instruct student model\",\n", + ")\n", + "\n", + "print()\n", + "\n", + "teacher_model = create_model(\n", + " hf_repo=\"meta-llama/Llama-3.2-3B-Instruct\",\n", + " model_name=\"llama-3-2-3b-teacher\",\n", + " description=\"Llama 3.2 3B Instruct teacher model\",\n", + ")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Phase 1: Fine-Tune the Teacher\n", + "\n", + "### 5. Train Teacher with Full SFT\n", + "\n", + "For best distillation results, fine-tune the teacher on the **same dataset** that will be used for distillation. This ensures the teacher has learned the task-specific knowledge that the student will inherit.\n", + "\n", + "We train the 3B Instruct model with Full SFT on the SQuAD dataset." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from nemo_automodel_plugin.schema import AutomodelJobInput\n", + "\n", + "job_suffix = uuid.uuid4().hex[:4]\n", + "\n", + "TEACHER_JOB_NAME = f\"teacher-sft-job-{job_suffix}\"\n", + "TEACHER_OUTPUT_NAME = f\"teacher-model-{job_suffix}\"\n", + "\n", + "teacher_spec = AutomodelJobInput(\n", + " model=f\"default/{teacher_model.name}\",\n", + " dataset={\"training\": f\"default/{DATASET_NAME}\"},\n", + " training={\n", + " \"training_type\": \"sft\",\n", + " \"finetuning_type\": \"all_weights\",\n", + " \"max_seq_length\": 2048,\n", + " },\n", + " schedule={\"epochs\": 1},\n", + " batch={\"global_batch_size\": 64, \"micro_batch_size\": 1},\n", + " optimizer={\"learning_rate\": 5e-5},\n", + " parallelism={\"num_gpus_per_node\": 1},\n", + " output={\"name\": TEACHER_OUTPUT_NAME},\n", + ")\n", + "\n", + "teacher_job = client.customization.automodel.jobs.create(\n", + " spec=teacher_spec, workspace=\"default\", name=TEACHER_JOB_NAME\n", + ")\n", + "\n", + "TRAINED_TEACHER_NAME = TEACHER_OUTPUT_NAME\n", + "print(f\"Teacher training job: {teacher_job.job.name}\")\n", + "print(f\"Output teacher model: {TRAINED_TEACHER_NAME}\")\n" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from IPython.display import clear_output\n", + "\n", + "\n", + "def wait_for_job(job_name: str):\n", + " \"\"\"Poll job status until completion.\"\"\"\n", + " while True:\n", + " status = client.jobs.get_status(name=job_name, workspace=\"default\")\n", + " clear_output(wait=True)\n", + " print(f\"Job: {job_name}\")\n", + " print(f\"Status: {status.status}\")\n", + "\n", + " for job_step in status.steps or []:\n", + " if job_step.name == \"training\":\n", + " for task in job_step.tasks or []:\n", + " details = task.status_details or {}\n", + " step = details.get(\"step\")\n", + " max_steps = details.get(\"max_steps\")\n", + " if step is not None and max_steps is not None:\n", + " print(f\"Progress: Step {step}/{max_steps} ({step / max_steps * 100:.1f}%)\")\n", + " phase = details.get(\"phase\")\n", + " if phase:\n", + " print(f\"Phase: {phase}\")\n", + " break\n", + " break\n", + "\n", + " if status.status in (\"completed\", \"failed\", \"cancelled\", \"error\"):\n", + " print(f\"\\nJob finished: {status.status}\")\n", + " return status\n", + "\n", + " time.sleep(10)\n", + "\n", + "\n", + "teacher_status = wait_for_job(TEACHER_JOB_NAME)\n", + "assert teacher_status.status == \"completed\"" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Phase 2: Establish Baseline (Base Student)\n", + "\n", + "### 6. Deploy the Base Student Model\n", + "\n", + "Before distillation, deploy the base student model (1B Instruct, without any fine-tuning) to establish a baseline ROUGE score. After distillation, we compare the distilled student against this baseline to measure improvement." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "baseline_suffix = uuid.uuid4().hex[:4]\n", + "BASELINE_DEPLOYMENT_CONFIG = f\"baseline-student-cfg-{baseline_suffix}\"\n", + "BASELINE_DEPLOYMENT_NAME = f\"baseline-student-{baseline_suffix}\"\n", + "\n", + "baseline_deployment_config = client.inference.deployment_configs.create(\n", + " workspace=\"default\",\n", + " name=BASELINE_DEPLOYMENT_CONFIG,\n", + " engine=\"vllm\",\n", + " model_spec={\n", + " \"model_namespace\": \"default\",\n", + " \"model_name\": student_model.name,\n", + " },\n", + " executor_config={\n", + " \"gpu\": 1,\n", + " \"image_name\": \"vllm/vllm-openai\",\n", + " \"image_tag\": \"v0.22.1\",\n", + " },\n", + ")\n", + "\n", + "baseline_deployment = client.inference.deployments.create(\n", + " workspace=\"default\",\n", + " name=BASELINE_DEPLOYMENT_NAME,\n", + " config=baseline_deployment_config.name\n", + ")\n", + "\n", + "print(f\"Baseline student deployment: {baseline_deployment.name}\")\n" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "def wait_for_deployment(deployment_name: str, timeout_minutes: int = 30):\n", + " \"\"\"Poll deployment until ready.\"\"\"\n", + " start = time.time()\n", + " timeout = timeout_minutes * 60\n", + " while True:\n", + " dep = client.inference.deployments.retrieve(name=deployment_name, workspace=\"default\")\n", + " elapsed = time.time() - start\n", + " clear_output(wait=True)\n", + " print(f\"Deployment: {deployment_name}\")\n", + " print(f\"Status: {dep.status}\")\n", + " print(f\"Elapsed: {int(elapsed // 60)}m {int(elapsed % 60)}s\")\n", + "\n", + " if dep.status == \"READY\":\n", + " print(\"\\nDeployment is ready!\")\n", + " remaining = int(timeout - elapsed)\n", + " if remaining <= 0:\n", + " raise TimeoutError(f\"Deployment timeout after {timeout_minutes} minutes\")\n", + " if not client.models.wait_for_status(\n", + " deployment_name=deployment_name,\n", + " desired_status=\"READY\",\n", + " workspace=\"default\",\n", + " timeout=remaining,\n", + " check_gateway=True,\n", + " ):\n", + " raise TimeoutError(\"Inference gateway did not become ready\")\n", + " return dep\n", + " if dep.status in (\"FAILED\", \"ERROR\", \"TERMINATED\", \"LOST\"):\n", + " raise RuntimeError(f\"Deployment failed with status: {dep.status}\")\n", + " if elapsed > timeout:\n", + " raise TimeoutError(f\"Deployment timeout after {timeout_minutes} minutes\")\n", + " time.sleep(15)\n", + "\n", + "\n", + "try:\n", + " dep_status = wait_for_deployment(BASELINE_DEPLOYMENT_NAME)\n", + " assert dep_status.status == \"READY\"\n", + "except Exception:\n", + " # Free GPUs if readiness fails before the later baseline-cleanup cell runs.\n", + " try:\n", + " client.inference.deployments.delete(name=BASELINE_DEPLOYMENT_NAME, workspace=\"default\")\n", + " if not client.models.wait_for_status(\n", + " deployment_name=BASELINE_DEPLOYMENT_NAME,\n", + " desired_status=\"DELETED\",\n", + " workspace=\"default\",\n", + " timeout=600,\n", + " ):\n", + " raise TimeoutError(\n", + " f\"Deployment {BASELINE_DEPLOYMENT_NAME} was not deleted within timeout\"\n", + " )\n", + " client.inference.deployment_configs.delete(name=BASELINE_DEPLOYMENT_CONFIG, workspace=\"default\")\n", + " except Exception as cleanup_error:\n", + " print(f\"Baseline cleanup after readiness failure also failed: {cleanup_error}\")\n", + " raise" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7. Generate Baseline Predictions on Test Set" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "with open(f\"{DATASET_PATH}/testing.jsonl\", \"r\", encoding=\"utf-8\") as f:\n", + " test_data = [json.loads(line) for line in f]\n", + "\n", + "contexts = [row[\"context\"] for row in test_data]\n", + "questions = [row[\"question\"] for row in test_data]\n", + "reference_completions = [row[\"completion\"] for row in test_data]\n", + "\n", + "print(f\"Test samples: {len(contexts)}\")\n", + "print(f\"Sample context: {contexts[0]}\")\n", + "print(f\"Sample question: {questions[0]}\")\n", + "print(f\"Sample reference: {reference_completions[0]}\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "def generate_completions(\n", + " deployment_name: str,\n", + " output_model_name: str,\n", + " contexts: list[str],\n", + " questions: list[str],\n", + ") -> list[str]:\n", + " \"\"\"Generate completions for a list of context/question pairs using a deployed model.\"\"\"\n", + " completions = []\n", + " for context, question in zip(contexts, questions):\n", + " messages = [\n", + " {\n", + " \"role\": \"user\",\n", + " \"content\": f\"Based on the following context, answer the question.\\n\\nContext: {context}\\n\\nQuestion: {question}\",\n", + " }\n", + " ]\n", + " response = client.inference.gateway.provider.post(\n", + " \"v1/chat/completions\",\n", + " name=deployment_name,\n", + " workspace=\"default\",\n", + " body={\n", + " \"model\": f\"default/{output_model_name}\",\n", + " \"messages\": messages,\n", + " \"temperature\": 0,\n", + " \"max_tokens\": 128,\n", + " }\n", + " )\n", + " completions.append(response[\"choices\"][0][\"message\"][\"content\"])\n", + " return completions\n", + "\n", + "\n", + "print(\"Generating baseline (base student) predictions...\")\n", + "baseline_completions = generate_completions(BASELINE_DEPLOYMENT_NAME, student_model.name, contexts, questions)\n", + "print(f\"Generated {len(baseline_completions)} baseline predictions\")\n", + "print(f\"\\nSample baseline output: {baseline_completions[0]}\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 8. Delete Baseline Deployment\n", + "\n", + "Delete the baseline student deployment to free GPU resources for the distillation training job and subsequent distilled model deployment." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "client.inference.deployments.delete(name=BASELINE_DEPLOYMENT_NAME, workspace=\"default\")\n", + "print(f\"Deleted baseline deployment: {BASELINE_DEPLOYMENT_NAME}\")\n", + "\n", + "if not client.models.wait_for_status(\n", + " deployment_name=BASELINE_DEPLOYMENT_NAME,\n", + " desired_status=\"DELETED\",\n", + " workspace=\"default\",\n", + " timeout=600,\n", + "):\n", + " raise TimeoutError(\n", + " f\"Deployment {BASELINE_DEPLOYMENT_NAME} was not deleted within timeout\"\n", + " )\n", + "\n", + "client.inference.deployment_configs.delete(name=BASELINE_DEPLOYMENT_CONFIG, workspace=\"default\")\n", + "print(f\"Deleted baseline deployment config: {BASELINE_DEPLOYMENT_CONFIG}\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Phase 3: Distill into Student\n", + "\n", + "### 9. Create Knowledge Distillation Job\n", + "\n", + "Now create a distillation job that trains the 1B student using the **fine-tuned** 3B teacher's output distribution. The `model` field specifies the student, and `teacher_model` references the trained teacher model entity from Phase 1." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**GPU Requirements:**\n", + "\n", + "KD requires loading both student and teacher models, so plan GPU memory accordingly:\n", + "- 1B student + 3B teacher: 1 GPU (24GB+ VRAM each)\n", + "- 3B student + 8B teacher: 4 GPUs\n", + "- 8B student + 70B teacher: 8+ GPUs\n", + "\n", + "Use `teacher_precision=\"bf16\"` (default) to reduce teacher memory footprint." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from nemo_automodel_plugin.schema import AutomodelJobInput\n", + "\n", + "KD_JOB_NAME = f\"my-kd-job-{job_suffix}\"\n", + "KD_OUTPUT_NAME = f\"kd-student-{job_suffix}\"\n", + "\n", + "kd_spec = AutomodelJobInput(\n", + " model=f\"default/{student_model.name}\",\n", + " dataset={\"training\": f\"default/{DATASET_NAME}\"},\n", + " training={\n", + " \"training_type\": \"distillation\",\n", + " \"finetuning_type\": \"all_weights\",\n", + " \"teacher_model\": f\"default/{TRAINED_TEACHER_NAME}\",\n", + " \"teacher_precision\": \"bf16\",\n", + " \"distillation_ratio\": 0.5,\n", + " \"distillation_temperature\": 2.0,\n", + " \"max_seq_length\": 2048,\n", + " },\n", + " schedule={\"epochs\": 1},\n", + " batch={\"global_batch_size\": 64, \"micro_batch_size\": 1},\n", + " optimizer={\"learning_rate\": 5e-5},\n", + " parallelism={\"num_gpus_per_node\": 1},\n", + " output={\"name\": KD_OUTPUT_NAME},\n", + ")\n", + "\n", + "kd_job = client.customization.automodel.jobs.create(\n", + " spec=kd_spec, workspace=\"default\", name=KD_JOB_NAME\n", + ")\n", + "\n", + "DISTILLED_STUDENT_NAME = KD_OUTPUT_NAME\n", + "print(f\"Distillation job: {kd_job.job.name}\")\n", + "print(f\"Output student model: {DISTILLED_STUDENT_NAME}\")\n" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 10. Track Distillation Progress" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "kd_status = wait_for_job(KD_JOB_NAME)\n", + "assert kd_status.status == \"completed\"" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "---\n", + "\n", + "## Phase 4: Evaluate the Distilled Student Model\n", + "\n", + "### 11. Deploy the Distilled Student Model\n", + "\n", + "The output model has the same architecture as the 1B student—only its weights have been updated via distillation. It requires just 1 GPU to deploy." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "deploy_suffix_2 = uuid.uuid4().hex[:4]\n", + "STUDENT_DEPLOYMENT_CONFIG = f\"kd-student-deploy-cfg-{deploy_suffix_2}\"\n", + "STUDENT_DEPLOYMENT_NAME = f\"kd-student-deploy-{deploy_suffix_2}\"\n", + "\n", + "student_deployment_config = client.inference.deployment_configs.create(\n", + " workspace=\"default\",\n", + " name=STUDENT_DEPLOYMENT_CONFIG,\n", + " engine=\"vllm\",\n", + " model_spec={\n", + " \"model_namespace\": \"default\",\n", + " \"model_name\": DISTILLED_STUDENT_NAME,\n", + " },\n", + " executor_config={\n", + " \"gpu\": 1,\n", + " \"image_name\": \"vllm/vllm-openai\",\n", + " \"image_tag\": \"v0.22.1\",\n", + " },\n", + ")\n", + "\n", + "student_deployment = client.inference.deployments.create(\n", + " workspace=\"default\",\n", + " name=STUDENT_DEPLOYMENT_NAME,\n", + " config=student_deployment_config.name\n", + ")\n", + "\n", + "print(f\"Student deployment: {student_deployment.name}\")\n" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "wait_for_deployment(STUDENT_DEPLOYMENT_NAME)" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 12. Generate Student Predictions on Test Set" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "print(\"Generating distilled student predictions...\")\n", + "student_completions = generate_completions(STUDENT_DEPLOYMENT_NAME, DISTILLED_STUDENT_NAME, contexts, questions)\n", + "print(f\"Generated {len(student_completions)} student predictions\")\n", + "print(f\"\\nSample student output: {student_completions[0]}\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 13. Compute ROUGE Scores\n", + "\n", + "Compare the base student (before distillation) and the distilled student against the ground-truth reference completions using ROUGE metrics." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import evaluate\n", + "\n", + "rouge = evaluate.load(\"rouge\")\n", + "\n", + "baseline_scores = rouge.compute(predictions=baseline_completions, references=reference_completions)\n", + "student_scores = rouge.compute(predictions=student_completions, references=reference_completions)\n", + "\n", + "metrics = list(baseline_scores.keys())\n", + "header = f\"{'Model':<35} \" + \" \".join(f\"{m:>10}\" for m in metrics)\n", + "separator = \"-\" * len(header)\n", + "\n", + "print(\"=\" * 60)\n", + "print(\"ROUGE SCORE COMPARISON\")\n", + "print(\"=\" * 60)\n", + "print(header)\n", + "print(separator)\n", + "print(f\"{'Base Student (1B, no training)':<35} \" + \" \".join(f\"{baseline_scores[m]:>10.4f}\" for m in metrics))\n", + "print(f\"{'Distilled Student (1B, KD)':<35} \" + \" \".join(f\"{student_scores[m]:>10.4f}\" for m in metrics))" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "print(\"Sample predictions (first 3):\\n\")\n", + "for i in range(min(3, len(contexts))):\n", + " print(f\"--- Sample {i + 1} ---\")\n", + " print(f\"Question: {questions[i]}\")\n", + " print(f\"Reference: {reference_completions[i]}\")\n", + " print(f\"Baseline: {baseline_completions[i][:200]}\")\n", + " print(f\"Distilled: {student_completions[i][:200]}\")\n", + " print()" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Interpreting ROUGE Scores:**\n", + "\n", + "| Metric | Measures |\n", + "|--------|----------|\n", + "| **ROUGE-1** | Unigram overlap between prediction and reference |\n", + "| **ROUGE-2** | Bigram overlap (captures phrase-level similarity) |\n", + "| **ROUGE-L** | Longest common subsequence (captures sentence structure) |\n", + "| **ROUGE-Lsum** | ROUGE-L computed over full summaries |\n", + "\n", + "**What to expect:**\n", + "- The base student (1B, no training) provides a lower bound since it has not seen the task data\n", + "- The distilled student (1B, KD) should significantly outperform the base student, demonstrating the knowledge transferred from the 3B teacher\n", + "- If the distilled student scores are not much higher than the baseline, try increasing `distillation_temperature`, adjusting `distillation_ratio`, or training for more epochs\n", + "\n", + "---\n", + "\n", + "## Hyperparameters\n", + "\n", + "For detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the [Hyperparameter Reference](../manage-customization-jobs/hyperparameters.md).\n", + "\n", + "---\n", + "\n", + "## Troubleshooting\n", + "\n", + "**Job fails during model download:**\n", + "- Verify authentication secrets are configured (refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md))\n", + "- For gated Hugging Face models (Llama, Gemma), accept the license on the model page\n", + "- Check both `model` (student) and `teacher_model` URNs are correct\n", + "- Ensure both model entities exist: `client.models.retrieve(name=..., workspace=\"default\")`\n", + "\n", + "**Job fails with OOM (Out of Memory) error:**\n", + "\n", + "KD loads both models, so OOM is more likely than with SFT:\n", + "1. **First try:** Use `teacher_precision=\"bf16\"` to reduce teacher memory\n", + "2. **Still OOM:** Reduce `micro_batch_size` to 1\n", + "3. **Still OOM:** Reduce `global_batch_size` and `max_seq_length`\n", + "4. **Last resort:** Increase `num_gpus_per_node`\n", + "\n", + "**No chat template / `/chat/completions` fails:**\n", + "- Use Instruct model variants (e.g., `Llama-3.2-1B-Instruct`) instead of base models (`Llama-3.2-1B`). Base models do not include a chat template in their tokenizer, so the output model will also lack one.\n", + "\n", + "**Distilled model quality is poor:**\n", + "- Increase `distillation_temperature` (try 2.0–5.0) to transfer more nuanced knowledge\n", + "- Adjust `distillation_ratio`—if dataset labels are high-quality, lower the ratio; if the teacher is strong, raise it\n", + "- Increase `epochs` or `max_steps` for more training\n", + "- Verify teacher and student share the same vocabulary\n", + "\n", + "**Vocabulary mismatch error:**\n", + "- Teacher and student must use the same tokenizer. Use models from the same family (e.g., Llama 3.2 1B Instruct + Llama 3.2 3B Instruct)\n", + "\n", + "**Deployment fails:**\n", + "- Verify output model exists: `client.models.retrieve(name=DISTILLED_STUDENT_NAME, workspace=\"default\")`\n", + "- Check deployment logs: `client.inference.deployments.get_logs(name=deployment.name, workspace=\"default\")`\n", + "- The distilled model has the same size as the student, so GPU requirements match the student model\n", + "\n", + "\n", + "## Next Steps\n", + "\n", + "- [Monitor training metrics](fine-tune-metrics) in detail\n", + "- [Evaluate your fine-tuned model](../../evaluator/index) using the Evaluator service\n", + "- Learn about [LoRA customization](./lora-customization-job) for resource-efficient fine-tuning\n", + "- Learn about [Full SFT](./sft-customization-job) for direct supervised fine-tuning" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file diff --git a/docs/customizer/tutorials/distillation-customization-job.mdx b/docs/customizer/tutorials/distillation-customization-job.mdx index 8da102ae5f..fa4dec2cb6 100644 --- a/docs/customizer/tutorials/distillation-customization-job.mdx +++ b/docs/customizer/tutorials/distillation-customization-job.mdx @@ -5,8 +5,6 @@ description: "" [Run in Google Colab](https://colab.research.google.com/github/NVIDIA-NeMo/nemo-platform/blob/main/docs/customizer/tutorials/distillation-customization-job.ipynb) -# Knowledge Distillation Customization - Learn how to train a smaller student model to mimic a larger teacher model using knowledge distillation (KD). ## About @@ -67,6 +65,8 @@ Before starting this tutorial, ensure you have: pip install evaluate rouge_score datasets ``` +4. **At least one GPU with CUDA 13+** + ## Quick Start ### 1. Initialize SDK @@ -189,16 +189,16 @@ print(json.dumps([f.model_dump() for f in client.files.list(fileset=DATASET_NAME ### 3. Secrets Setup -In this tutorial we use two Llama 3.2 Instruct models from HuggingFace: +In this tutorial we use two Llama 3.2 Instruct models from Hugging Face: - **Teacher:** [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct) (3B parameters) - **Student:** [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) (1B parameters) Both models share the same tokenizer/vocabulary (required for knowledge distillation) and include a chat template for deployment with `/chat/completions`. -**HuggingFace Authentication:** -- For gated models (Llama, Gemma), you must provide a HuggingFace token via the `token_secret` parameter -- Get your token from [HuggingFace Settings](https://huggingface.co/settings/tokens) (requires Read access) -- Accept the model's terms on the HuggingFace model page before using it: +**Hugging Face Authentication:** +- For gated models (Llama, Gemma), you must provide a Hugging Face token via the `token_secret` parameter +- Get your token from [Hugging Face Settings](https://huggingface.co/settings/tokens) (requires Read access) +- Accept the model's terms on the Hugging Face model page before using it: - [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) - [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct) @@ -433,18 +433,45 @@ def wait_for_deployment(deployment_name: str, timeout_minutes: int = 30): if dep.status == "READY": print("\nDeployment is ready!") + remaining = int(timeout - elapsed) + if remaining <= 0: + raise TimeoutError(f"Deployment timeout after {timeout_minutes} minutes") + if not client.models.wait_for_status( + deployment_name=deployment_name, + desired_status="READY", + workspace="default", + timeout=remaining, + check_gateway=True, + ): + raise TimeoutError("Inference gateway did not become ready") return dep if dep.status in ("FAILED", "ERROR", "TERMINATED", "LOST"): - print(f"\nDeployment failed: {dep.status}") - return dep + raise RuntimeError(f"Deployment failed with status: {dep.status}") if elapsed > timeout: - print(f"\nTimeout ({timeout_minutes}m). Check status manually.") - return dep + raise TimeoutError(f"Deployment timeout after {timeout_minutes} minutes") time.sleep(15) -dep_status = wait_for_deployment(BASELINE_DEPLOYMENT_NAME) -assert dep_status.status == "READY" +try: + dep_status = wait_for_deployment(BASELINE_DEPLOYMENT_NAME) + assert dep_status.status == "READY" +except Exception: + # Free GPUs if readiness fails before the later baseline-cleanup cell runs. + try: + client.inference.deployments.delete(name=BASELINE_DEPLOYMENT_NAME, workspace="default") + if not client.models.wait_for_status( + deployment_name=BASELINE_DEPLOYMENT_NAME, + desired_status="DELETED", + workspace="default", + timeout=600, + ): + raise TimeoutError( + f"Deployment {BASELINE_DEPLOYMENT_NAME} was not deleted within timeout" + ) + client.inference.deployment_configs.delete(name=BASELINE_DEPLOYMENT_CONFIG, workspace="default") + except Exception as cleanup_error: + print(f"Baseline cleanup after readiness failure also failed: {cleanup_error}") + raise ``` ### 7. Generate Baseline Predictions on Test Set @@ -694,7 +721,7 @@ For detailed information on all available hyperparameters, recommended values, a **Job fails during model download:** - Verify authentication secrets are configured (refer to [Managing Secrets](/documentation/get-started/core-concepts/manage-secrets)) -- For gated HuggingFace models (Llama, Gemma), accept the license on the model page +- For gated Hugging Face models (Llama, Gemma), accept the license on the model page - Check both `model` (student) and `teacher_model` URNs are correct - Ensure both model entities exist: `client.models.retrieve(name=..., workspace="default")` diff --git a/docs/customizer/tutorials/dpo-customization-job.ipynb b/docs/customizer/tutorials/dpo-customization-job.ipynb index d9a56d4f64..5c8437dfcb 100644 --- a/docs/customizer/tutorials/dpo-customization-job.ipynb +++ b/docs/customizer/tutorials/dpo-customization-job.ipynb @@ -1,515 +1,528 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "\n", - "\n", - "# DPO Model Customization Job\n", - "\n", - "Learn how to use the NeMo Platform to align a model with **DPO** (Direct Preference Optimization) on a preference dataset. For each prompt, DPO trains on a *chosen* (preferred) and a *rejected* response so the model prefers the chosen style — no separate reward model required.\n", - "\n", - "This tutorial uses the `rl` customization backend (powered by [NVIDIA NeMo-RL](https://github.com/NVIDIA-NeMo/RL)), which runs DPO on a **Ray** cluster. Unlike the [SFT](./sft-customization-job) and [LoRA](./lora-customization-job) tutorials (Docker GPU jobs), `rl` requires a **Kubernetes-backed** NeMo Platform. DPO here is **full-weight** (no LoRA/adapter); the output is a full model entity.\n", - "\n", - "**Time to complete:** approximately 45-60 minutes. Job duration increases with model and dataset size." - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "# DPO Model Customization Job\n", + "\n", + "Learn how to use the NeMo Platform to align a model with **DPO** (Direct Preference Optimization) on a preference dataset. For each prompt, DPO trains on a *chosen* (preferred) and a *rejected* response so the model prefers the chosen style — no separate reward model required.\n", + "\n", + "This tutorial uses the `rl` customization backend (powered by [NVIDIA NeMo-RL](https://github.com/NVIDIA-NeMo/RL)), which runs DPO on a **Ray** cluster. Unlike the [SFT](/documentation/customizer-reference/tutorials/sft-customization-job) and [LoRA](/documentation/customizer-reference/tutorials/lora-customization-job) tutorials (Docker GPU jobs), `rl` requires a **Kubernetes-backed** NeMo Platform. DPO here is **full-weight** (no LoRA/adapter); the output is a full model entity.\n", + "\n", + "**Time to complete:** approximately 45-60 minutes. Job duration increases with model and dataset size." + ], + "id": "40916acf" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "Before starting this tutorial, ensure you have:\n", + "\n", + "1. **Completed the [Quickstart](/documentation/get-started)** to install the NeMo Platform and Python SDK.\n", + "2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root).\n", + "3. **Installed the `datasets` package**: `pip install datasets`.\n", + "4. **A platform configured with `platform.runtime: kubernetes`.** The `rl` (DPO) backend provisions a Ray cluster and has **no local Docker fallback** — `submit` fails fast on a Docker-runtime platform. Multi-node jobs (`parallelism.num_nodes > 1`) additionally require the platform-side `NMP_RL_MULTINODE_SHARED_STORAGE_PATH`.\n", + "5. **A Hugging Face token** with access to the gated base model (this tutorial uses `meta-llama/Llama-3.2-1B-Instruct`). Export it as `HF_TOKEN`.\n", + "6. **At least one GPU with CUDA 13+** and a GPU execution profile (`nemo jobs list-execution-profiles`)." + ], + "id": "15435f3c" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Quick Start\n", + "\n", + "### 1. Initialize the SDK\n", + "\n", + "The SDK needs your NeMo Platform server URL. By default `http://localhost:8080` is used; set `NMP_BASE_URL` to override:\n", + "\n", + "```sh\n", + "export NMP_BASE_URL=\n", + "```" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import json\n", + "import os\n", + "import time\n", + "import uuid\n", + "from pathlib import Path\n", + "from nemo_platform import NeMoPlatform, ConflictError\n", + "from nemo_platform.types.secrets import PlatformSecretResponse\n", + "from nemo_platform.types.files import HuggingfaceStorageConfigParam\n", + "from nemo_rl_plugin.schema import RlJobInput\n", + "\n", + "\n", + "def max_wait_time_checker(seconds: int, label: str = \"\"):\n", + " \"\"\"Return a check() that raises TimeoutError once `seconds` have elapsed.\"\"\"\n", + " start = time.time()\n", + "\n", + " def check():\n", + " if time.time() - start > seconds:\n", + " raise TimeoutError(f\"{label} took longer than {seconds} seconds\")\n", + "\n", + " return check\n", + "\n", + "\n", + "NMP_BASE_URL = os.environ.get(\"NMP_BASE_URL\", \"http://localhost:8080\")\n", + "sdk = NeMoPlatform(base_url=NMP_BASE_URL, workspace=\"default\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Prepare the Preference Dataset\n", + "\n", + "DPO trains on **preference data**. The `rl` backend takes a **single** dataset fileset that holds both `training.jsonl` and `validation.jsonl`, and auto-detects the row schema from the first line. Three preference formats are supported (see the platform's `BinaryPreferenceDatasetItemSchema` / `HelpSteer3DatasetItemSchema` / `Tulu3PreferenceDatasetItemSchema`):" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Binary Preference Format\n", + "\n", + "Simple `prompt` / `chosen` / `rejected` (the `prompt` may be a string or a list of chat messages):\n", + "\n", + "```json\n", + "{\"prompt\": \"What is the capital of France?\", \"chosen\": \"The capital of France is Paris.\", \"rejected\": \"I'm not sure.\"}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### HelpSteer3 Format (used here)\n", + "\n", + "A conversation `context` (string or chat messages), two candidate `response1` / `response2`, and a signed `overall_preference` in -3..3 — **negative** means response 1 is preferred, **positive** means response 2, **0** is a tie. This is the **raw** schema of `nvidia/HelpSteer3`, so no conversion is needed:\n", + "\n", + "```json\n", + "{\"context\": [{\"role\": \"user\", \"content\": \"Explain how to use git rebase\"}], \"response1\": \"...\", \"response2\": \"...\", \"overall_preference\": -2}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Tulu3 Preference Format\n", + "\n", + "Full chat conversations for both the chosen and rejected branches (each a list of messages ending with the assistant turn):\n", + "\n", + "```json\n", + "{\"chosen\": [{\"role\": \"user\", \"content\": \"...\"}, {\"role\": \"assistant\", \"content\": \"preferred\"}], \"rejected\": [{\"role\": \"user\", \"content\": \"...\"}, {\"role\": \"assistant\", \"content\": \"dispreferred\"}]}\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Download nvidia/HelpSteer3\n", + "\n", + "We use [nvidia/HelpSteer3](https://huggingface.co/datasets/nvidia/HelpSteer3) (the `preference` subset), NVIDIA's open preference dataset. It ships native `train` and `validation` splits and matches the HelpSteer3 schema above, so we upload the rows **as-is** — the platform's `HelpSteer3Dataset` loader handles the `overall_preference` semantics (including ties) at training time." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from datasets import load_dataset, Dataset\n", + "\n", + "print(\"Loading dataset nvidia/HelpSteer3 (preference subset)\")\n", + "ds = load_dataset(\"nvidia/HelpSteer3\", \"preference\")\n", + "\n", + "# Small subsets keep the tutorial fast; larger sets train better but take longer.\n", + "training_size = 3000\n", + "validation_size = 300\n", + "DATASET_NAME = \"dpo-dataset\"\n", + "DATASET_PATH = Path(\"dpo-dataset\").absolute()\n", + "os.makedirs(DATASET_PATH, exist_ok=True)\n", + "\n", + "train_dataset = ds[\"train\"]\n", + "validation_dataset = ds[\"validation\"]\n", + "assert isinstance(train_dataset, Dataset) and isinstance(validation_dataset, Dataset)\n", + "\n", + "# Save raw HelpSteer3 rows directly — no conversion. The platform detects the\n", + "# HelpSteer3 schema from the row keys (context / response1 / response2 / overall_preference).\n", + "train_dataset.select(range(training_size)).to_json(f\"{DATASET_PATH}/training.jsonl\")\n", + "validation_dataset.select(range(validation_size)).to_json(f\"{DATASET_PATH}/validation.jsonl\")\n", + "\n", + "print(f\"Saved training.jsonl ({training_size} rows) and validation.jsonl ({validation_size} rows)\")\n", + "with open(f\"{DATASET_PATH}/training.jsonl\") as f:\n", + " sample = json.loads(f.readline())\n", + "print(\"Sample keys:\", sorted(sample.keys()))\n", + "print(\"overall_preference:\", sample[\"overall_preference\"])" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Create FileSet and Upload Preference Data\n", + "\n", + "Upload both JSONL files to a single FileSet so the DPO job can read them." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "try:\n", + " sdk.files.filesets.create(workspace=\"default\", name=DATASET_NAME, description=\"DPO preference data\")\n", + " print(f\"Created fileset: {DATASET_NAME}\")\n", + "except ConflictError:\n", + " print(f\"Fileset '{DATASET_NAME}' already exists, continuing...\")\n", + "\n", + "sdk.files.upload(local_path=DATASET_PATH, remote_path=\"\", fileset=DATASET_NAME, workspace=\"default\")\n", + "\n", + "print(\"Preference data:\")\n", + "print(json.dumps([f.model_dump() for f in sdk.files.list(fileset=DATASET_NAME, workspace=\"default\").data], indent=2, default=str))" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4. Secrets Setup\n", + "\n", + "The base model (`meta-llama/Llama-3.2-1B-Instruct`) is gated, so store your Hugging Face token as a platform secret named `hf-token` and reference it on the model fileset." + ], + "id": "7f8bde21" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "HF_TOKEN = os.getenv(\"HF_TOKEN\")\n", + "if not HF_TOKEN:\n", + " raise RuntimeError(\"Set HF_TOKEN before running this tutorial.\")\n", + "\n", + "def create_or_get_secret(name: str, value: str, label: str) -> PlatformSecretResponse:\n", + " try:\n", + " secret = sdk.secrets.create(name=name, workspace=\"default\", value=value)\n", + " print(f\"Created secret: {name}\")\n", + " return secret\n", + " except ConflictError:\n", + " print(f\"Secret '{name}' already exists, continuing...\")\n", + " return sdk.secrets.retrieve(name=name, workspace=\"default\")\n", + "\n", + "\n", + "hf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5. Create Base Model FileSet and Model Entity\n", + "\n", + "DPO starts from an instruction-tuned base model. The model entity's spec is inferred asynchronously after creation." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "HF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\n", + "MODEL_NAME = \"llama-3-2-1b-instruct\"\n", + "\n", + "storage = HuggingfaceStorageConfigParam(\n", + " type=\"huggingface\",\n", + " repo_id=HF_REPO_ID,\n", + " repo_type=\"model\",\n", + " token_secret=hf_secret.name,\n", + ")\n", + "\n", + "try:\n", + " base_model_fs = sdk.files.filesets.create(\n", + " workspace=\"default\", name=MODEL_NAME, description=\"Llama 3.2 1B Instruct base model\", storage=storage\n", + " )\n", + " print(f\"Created base model fileset: {MODEL_NAME}\")\n", + "except ConflictError:\n", + " base_model_fs = sdk.files.filesets.retrieve(workspace=\"default\", name=MODEL_NAME)\n", + " print(\"Base model fileset already exists.\")\n", + "\n", + "try:\n", + " base_model = sdk.models.create(workspace=\"default\", name=MODEL_NAME, fileset=f\"default/{MODEL_NAME}\")\n", + "except ConflictError:\n", + " base_model = sdk.models.retrieve(workspace=\"default\", name=MODEL_NAME)\n", + "\n", + "print(f\"Base model fileset: fileset://default/{base_model.name}\")\n", + "\n", + "# Wait for the ModelSpec to be inferred from the checkpoint.\n", + "check = max_wait_time_checker(600, \"Model spec\")\n", + "while not base_model.spec:\n", + " check()\n", + " time.sleep(10)\n", + " base_model = sdk.models.retrieve(workspace=\"default\", name=MODEL_NAME)\n", + "print(\"Model spec ready\")" + ], + "execution_count": null, + "outputs": [], + "id": "1b798ede" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 6. Create the DPO Customization Job\n", + "\n", + "Submit a DPO job to the `rl` backend with `RlJobInput`. Note the DPO-specific shape:\n", + "\n", + "- `model` is a string ref to the model entity; `dataset` is a **single** string ref to the preference fileset (holding both files).\n", + "- The training method is `{\"type\": \"dpo\", ...}` — full-weight, no `finetuning_type`/LoRA.\n", + "- `ref_policy_kl_penalty` is **β** (DPO paper): how strongly the policy stays tied to the reference model.\n", + "- `rl` auto-generates the job id (`rl-`); read it back from the response.\n", + "\n", + "Other configurable knobs: `optimizer_type`, `adam_eps`, `activation_checkpointing`, `keep_top_k`, `val_at_end`, `preference_loss_weight`, `sft_loss_weight`. Run `nemo customization rl explain` for the live schema." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "job_suffix = uuid.uuid4().hex[:8]\n", + "OUTPUT_NAME = f\"llama-3-2-1b-dpo-{job_suffix}\"\n", + "\n", + "spec = RlJobInput(\n", + " model=f\"default/{base_model.name}\",\n", + " dataset=f\"default/{DATASET_NAME}\",\n", + " training={\n", + " \"type\": \"dpo\",\n", + " \"epochs\": 1,\n", + " \"batch_size\": 16,\n", + " \"micro_batch_size\": 1,\n", + " \"learning_rate\": 5e-6,\n", + " \"max_seq_length\": 4096,\n", + " \"ref_policy_kl_penalty\": 0.1,\n", + " \"parallelism\": {\n", + " \"num_nodes\": 1,\n", + " \"num_gpus_per_node\": 1,\n", + " \"tensor_parallel_size\": 1,\n", + " \"pipeline_parallel_size\": 1,\n", + " },\n", + " },\n", + " output={\"name\": OUTPUT_NAME},\n", + ")\n", + "\n", + "# `rl` auto-generates the job id (rl-); do not pass name=.\n", + "job = sdk.customization.rl.jobs.create(spec=spec, workspace=\"default\")\n", + "print(f\"Job ID: {job.job.name}\")\n", + "print(f\"Output model: {OUTPUT_NAME}\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7. Track Training Progress\n", + "\n", + "The DPO job runs four steps: download -> **dpo-training** (Ray) -> upload -> model-entity. We poll the top-level job status and surface the training step's progress." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from IPython.display import clear_output\n", + "\n", + "check = max_wait_time_checker(7200, \"DPO job\")\n", + "while True:\n", + " check()\n", + " status = sdk.jobs.get_status(name=job.job.name, workspace=\"default\")\n", + " clear_output(wait=True)\n", + " print(f\"Job Status: {status.status}\")\n", + "\n", + " step = max_steps = phase = None\n", + " for job_step in status.steps or []:\n", + " if job_step.name == \"dpo-training\":\n", + " for task in job_step.tasks or []:\n", + " d = task.status_details or {}\n", + " step, max_steps, phase = d.get(\"step\"), d.get(\"max_steps\"), d.get(\"phase\")\n", + " break\n", + " break\n", + " if step is not None and max_steps:\n", + " print(f\"Training: Step {step}/{max_steps} ({100 * step / max_steps:.1f}%)\")\n", + " if phase:\n", + " print(f\"Phase: {phase}\")\n", + "\n", + " if status.status in (\"completed\", \"failed\", \"cancelled\", \"error\"):\n", + " print(f\"\\nJob finished: {status.status}\")\n", + " break\n", + " time.sleep(15)\n", + "\n", + "assert status.status == \"completed\"" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Interpreting DPO training metrics** (in `status_details.metrics`):\n", + "\n", + "- **`loss`** — the DPO loss; should trend down as the policy learns to separate chosen from rejected.\n", + "- **Reward margin** (chosen minus rejected reward) — should trend **up**: the model increasingly prefers chosen responses.\n", + "- **Validation `loss`** — watch for divergence from training loss (overfitting). Raise `ref_policy_kl_penalty` (β) or add `sft_loss_weight` if the policy drifts too far from the reference." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 8. Validate the Output Model\n", + "\n", + "DPO produces a **full-weight model entity** (not an adapter). Confirm it was registered." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "model_entity = sdk.models.retrieve(workspace=\"default\", name=OUTPUT_NAME)\n", + "print(model_entity.model_dump_json(indent=2))" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 9. Deploy and Evaluate (optional)\n", + "\n", + "The DPO output is a full model, so it deploys like any full-weight checkpoint (see the [Full SFT](/documentation/customizer-reference/tutorials/sft-customization-job) tutorial for details). We deploy with vLLM and send a chat completion." + ], + "id": "fbbce3cb" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "deploy_suffix = uuid.uuid4().hex[:8]\n", + "DEPLOYMENT_CONFIG_NAME = f\"dpo-deployment-cfg-{deploy_suffix}\"\n", + "DEPLOYMENT_NAME = f\"dpo-deployment-{deploy_suffix}\"\n", + "\n", + "deployment_config = sdk.inference.deployment_configs.create(\n", + " workspace=\"default\",\n", + " name=DEPLOYMENT_CONFIG_NAME,\n", + " engine=\"vllm\",\n", + " model_spec={\"model_namespace\": \"default\", \"model_name\": OUTPUT_NAME},\n", + " executor_config={\"gpu\": 1, \"image_name\": \"vllm/vllm-openai\", \"image_tag\": \"v0.22.1\"},\n", + ")\n", + "\n", + "deployment = sdk.inference.deployments.create(\n", + " workspace=\"default\", name=DEPLOYMENT_NAME, config=deployment_config.name\n", + ")\n", + "print(f\"Deployment name: {deployment.name}\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "check = max_wait_time_checker(1800, \"Deployment\")\n", + "while True:\n", + " check()\n", + " deployment_status = sdk.inference.deployments.retrieve(name=deployment.name, workspace=\"default\")\n", + " clear_output(wait=True)\n", + " print(f\"Deployment status: {deployment_status.status}\")\n", + " deployment_state = str(deployment_status.status).lower()\n", + " if deployment_state in (\"ready\", \"running\"):\n", + " if not sdk.models.wait_for_gateway(deployment.name, workspace=\"default\", timeout=60):\n", + " raise RuntimeError(\"Inference gateway did not become ready\")\n", + " break\n", + " if deployment_state in (\"failed\", \"error\", \"terminated\", \"lost\"):\n", + " raise RuntimeError(f\"Deployment failed with status: {deployment_status.status}\")\n", + " time.sleep(15)" + ], + "execution_count": null, + "outputs": [], + "id": "a5811863" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "messages = [\n", + " {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n", + " {\"role\": \"user\", \"content\": \"Write a short, friendly email to a colleague asking to reschedule our meeting to Thursday.\"},\n", + "]\n", + "\n", + "response = sdk.inference.gateway.provider.post(\n", + " \"v1/chat/completions\",\n", + " name=deployment.name,\n", + " workspace=\"default\",\n", + " body={\"model\": f\"default/{OUTPUT_NAME}\", \"messages\": messages, \"temperature\": 0.7, \"max_tokens\": 256},\n", + ")\n", + "print(\"Model output:\\n\")\n", + "print(response[\"choices\"][0][\"message\"][\"content\"])" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "You aligned a base model with **DPO** on the NeMo Platform using the `rl` backend:\n", + "\n", + "- Uploaded a HelpSteer3 preference dataset **as-is** (the platform detects the schema natively).\n", + "- Submitted a full-weight DPO job that ran on a Ray cluster via the Kubernetes executor.\n", + "- Registered the output as a full model entity and (optionally) deployed it for inference.\n", + "\n", + "**Next steps:** tune the alignment strength with `ref_policy_kl_penalty` (β), add `sft_loss_weight` to anchor the policy to the chosen responses, enable `activation_checkpointing` for memory headroom, or scale up with `parallelism`. See the [Training Configuration](/documentation/customizer-reference/manage-customization-jobs/training-configuration) reference for the full hyperparameter set." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" + } }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Prerequisites\n", - "\n", - "Before starting this tutorial, ensure you have:\n", - "\n", - "1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install the NeMo Platform and Python SDK.\n", - "2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root).\n", - "3. **Installed the `datasets` package**: `pip install datasets`.\n", - "4. **A platform configured with `platform.runtime: kubernetes`.** The `rl` (DPO) backend provisions a Ray cluster and has **no local Docker fallback** — `submit` fails fast on a Docker-runtime platform. Multi-node jobs (`parallelism.num_nodes > 1`) additionally require the platform-side `NMP_RL_MULTINODE_SHARED_STORAGE_PATH`.\n", - "5. **A HuggingFace token** with access to the gated base model (this tutorial uses `meta-llama/Llama-3.2-1B-Instruct`). Export it as `HF_TOKEN`.\n", - "6. **At least one GPU with CUDA 12.8+** and a GPU execution profile (`nemo jobs list-execution-profiles`)." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Quick Start\n", - "\n", - "### 1. Initialize the SDK\n", - "\n", - "The SDK needs your NeMo Platform server URL. By default `http://localhost:8080` is used; set `NMP_BASE_URL` to override:\n", - "\n", - "```sh\n", - "export NMP_BASE_URL=\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import json\n", - "import os\n", - "import time\n", - "import uuid\n", - "from pathlib import Path\n", - "from nemo_platform import NeMoPlatform, ConflictError\n", - "from nemo_platform.types.secrets import PlatformSecretResponse\n", - "from nemo_platform.types.files import HuggingfaceStorageConfigParam\n", - "from nemo_rl_plugin.schema import RlJobInput\n", - "\n", - "\n", - "def max_wait_time_checker(seconds: int, label: str = \"\"):\n", - " \"\"\"Return a check() that raises TimeoutError once `seconds` have elapsed.\"\"\"\n", - " start = time.time()\n", - "\n", - " def check():\n", - " if time.time() - start > seconds:\n", - " raise TimeoutError(f\"{label} took longer than {seconds} seconds\")\n", - "\n", - " return check\n", - "\n", - "\n", - "NMP_BASE_URL = os.environ.get(\"NMP_BASE_URL\", \"http://localhost:8080\")\n", - "sdk = NeMoPlatform(base_url=NMP_BASE_URL, workspace=\"default\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2. Prepare the Preference Dataset\n", - "\n", - "DPO trains on **preference data**. The `rl` backend takes a **single** dataset fileset that holds both `training.jsonl` and `validation.jsonl`, and auto-detects the row schema from the first line. Three preference formats are supported (see the platform's `BinaryPreferenceDatasetItemSchema` / `HelpSteer3DatasetItemSchema` / `Tulu3PreferenceDatasetItemSchema`):" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Binary Preference Format\n", - "\n", - "Simple `prompt` / `chosen` / `rejected` (the `prompt` may be a string or a list of chat messages):\n", - "\n", - "```json\n", - "{\"prompt\": \"What is the capital of France?\", \"chosen\": \"The capital of France is Paris.\", \"rejected\": \"I'm not sure.\"}\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### HelpSteer3 Format (used here)\n", - "\n", - "A conversation `context` (string or chat messages), two candidate `response1` / `response2`, and a signed `overall_preference` in -3..3 — **negative** means response 1 is preferred, **positive** means response 2, **0** is a tie. This is the **raw** schema of `nvidia/HelpSteer3`, so no conversion is needed:\n", - "\n", - "```json\n", - "{\"context\": [{\"role\": \"user\", \"content\": \"Explain how to use git rebase\"}], \"response1\": \"...\", \"response2\": \"...\", \"overall_preference\": -2}\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Tulu3 Preference Format\n", - "\n", - "Full chat conversations for both the chosen and rejected branches (each a list of messages ending with the assistant turn):\n", - "\n", - "```json\n", - "{\"chosen\": [{\"role\": \"user\", \"content\": \"...\"}, {\"role\": \"assistant\", \"content\": \"preferred\"}], \"rejected\": [{\"role\": \"user\", \"content\": \"...\"}, {\"role\": \"assistant\", \"content\": \"dispreferred\"}]}\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Download nvidia/HelpSteer3\n", - "\n", - "We use [nvidia/HelpSteer3](https://huggingface.co/datasets/nvidia/HelpSteer3) (the `preference` subset), NVIDIA's open preference dataset. It ships native `train` and `validation` splits and matches the HelpSteer3 schema above, so we upload the rows **as-is** — the platform's `HelpSteer3Dataset` loader handles the `overall_preference` semantics (including ties) at training time." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from datasets import load_dataset, Dataset\n", - "\n", - "print(\"Loading dataset nvidia/HelpSteer3 (preference subset)\")\n", - "ds = load_dataset(\"nvidia/HelpSteer3\", \"preference\")\n", - "\n", - "# Small subsets keep the tutorial fast; larger sets train better but take longer.\n", - "training_size = 3000\n", - "validation_size = 300\n", - "DATASET_NAME = \"dpo-dataset\"\n", - "DATASET_PATH = Path(\"dpo-dataset\").absolute()\n", - "os.makedirs(DATASET_PATH, exist_ok=True)\n", - "\n", - "train_dataset = ds[\"train\"]\n", - "validation_dataset = ds[\"validation\"]\n", - "assert isinstance(train_dataset, Dataset) and isinstance(validation_dataset, Dataset)\n", - "\n", - "# Save raw HelpSteer3 rows directly — no conversion. The platform detects the\n", - "# HelpSteer3 schema from the row keys (context / response1 / response2 / overall_preference).\n", - "train_dataset.select(range(training_size)).to_json(f\"{DATASET_PATH}/training.jsonl\")\n", - "validation_dataset.select(range(validation_size)).to_json(f\"{DATASET_PATH}/validation.jsonl\")\n", - "\n", - "print(f\"Saved training.jsonl ({training_size} rows) and validation.jsonl ({validation_size} rows)\")\n", - "with open(f\"{DATASET_PATH}/training.jsonl\") as f:\n", - " sample = json.loads(f.readline())\n", - "print(\"Sample keys:\", sorted(sample.keys()))\n", - "print(\"overall_preference:\", sample[\"overall_preference\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3. Create FileSet and Upload Preference Data\n", - "\n", - "Upload both JSONL files to a single FileSet so the DPO job can read them." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "try:\n", - " sdk.files.filesets.create(workspace=\"default\", name=DATASET_NAME, description=\"DPO preference data\")\n", - " print(f\"Created fileset: {DATASET_NAME}\")\n", - "except ConflictError:\n", - " print(f\"Fileset '{DATASET_NAME}' already exists, continuing...\")\n", - "\n", - "sdk.files.upload(local_path=DATASET_PATH, remote_path=\"\", fileset=DATASET_NAME, workspace=\"default\")\n", - "\n", - "print(\"Preference data:\")\n", - "print(json.dumps([f.model_dump() for f in sdk.files.list(fileset=DATASET_NAME, workspace=\"default\").data], indent=2, default=str))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 4. Secrets Setup\n", - "\n", - "The base model (`meta-llama/Llama-3.2-1B-Instruct`) is gated, so store your HuggingFace token as a platform secret named `hf-token` and reference it on the model fileset." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "HF_TOKEN = os.getenv(\"HF_TOKEN\")\n", - "\n", - "def create_or_get_secret(name: str, value: str | None, label: str) -> PlatformSecretResponse | None:\n", - " if not value:\n", - " print(f\"{label} is not set - skipping secret (gated model downloads will fail without it)\")\n", - " return None\n", - " try:\n", - " secret = sdk.secrets.create(name=name, workspace=\"default\", value=value)\n", - " print(f\"Created secret: {name}\")\n", - " return secret\n", - " except ConflictError:\n", - " print(f\"Secret '{name}' already exists, continuing...\")\n", - " return sdk.secrets.retrieve(name=name, workspace=\"default\")\n", - "\n", - "\n", - "hf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 5. Create Base Model FileSet and Model Entity\n", - "\n", - "DPO starts from an instruction-tuned base model. The model entity's spec is inferred asynchronously after creation." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "HF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\n", - "MODEL_NAME = \"llama-3-2-1b-instruct\"\n", - "\n", - "storage = HuggingfaceStorageConfigParam(type=\"huggingface\", repo_id=HF_REPO_ID, repo_type=\"model\")\n", - "if hf_secret:\n", - " storage[\"token_secret\"] = hf_secret.name\n", - "\n", - "try:\n", - " base_model_fs = sdk.files.filesets.create(\n", - " workspace=\"default\", name=MODEL_NAME, description=\"Llama 3.2 1B Instruct base model\", storage=storage\n", - " )\n", - " print(f\"Created base model fileset: {MODEL_NAME}\")\n", - "except ConflictError:\n", - " base_model_fs = sdk.files.filesets.retrieve(workspace=\"default\", name=MODEL_NAME)\n", - " print(\"Base model fileset already exists.\")\n", - "\n", - "try:\n", - " base_model = sdk.models.create(workspace=\"default\", name=MODEL_NAME, fileset=f\"default/{MODEL_NAME}\")\n", - "except ConflictError:\n", - " base_model = sdk.models.retrieve(workspace=\"default\", name=MODEL_NAME)\n", - "\n", - "print(f\"Base model fileset: fileset://default/{base_model.name}\")\n", - "\n", - "# Wait for the ModelSpec to be inferred from the checkpoint.\n", - "check = max_wait_time_checker(600, \"Model spec\")\n", - "while not base_model.spec:\n", - " check()\n", - " time.sleep(10)\n", - " base_model = sdk.models.retrieve(workspace=\"default\", name=MODEL_NAME)\n", - "print(\"Model spec ready\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 6. Create the DPO Customization Job\n", - "\n", - "Submit a DPO job to the `rl` backend with `RlJobInput`. Note the DPO-specific shape:\n", - "\n", - "- `model` is a string ref to the model entity; `dataset` is a **single** string ref to the preference fileset (holding both files).\n", - "- The training method is `{\"type\": \"dpo\", ...}` — full-weight, no `finetuning_type`/LoRA.\n", - "- `ref_policy_kl_penalty` is **β** (DPO paper): how strongly the policy stays tied to the reference model.\n", - "- `rl` auto-generates the job id (`rl-`); read it back from the response.\n", - "\n", - "Other configurable knobs: `optimizer_type`, `adam_eps`, `activation_checkpointing`, `keep_top_k`, `val_at_end`, `preference_loss_weight`, `sft_loss_weight`. Run `nemo customization rl explain` for the live schema." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "job_suffix = uuid.uuid4().hex[:8]\n", - "OUTPUT_NAME = f\"llama-3-2-1b-dpo-{job_suffix}\"\n", - "\n", - "spec = RlJobInput(\n", - " model=f\"default/{base_model.name}\",\n", - " dataset=f\"default/{DATASET_NAME}\",\n", - " training={\n", - " \"type\": \"dpo\",\n", - " \"epochs\": 1,\n", - " \"batch_size\": 16,\n", - " \"micro_batch_size\": 1,\n", - " \"learning_rate\": 5e-6,\n", - " \"max_seq_length\": 4096,\n", - " \"ref_policy_kl_penalty\": 0.1,\n", - " \"parallelism\": {\n", - " \"num_nodes\": 1,\n", - " \"num_gpus_per_node\": 1,\n", - " \"tensor_parallel_size\": 1,\n", - " \"pipeline_parallel_size\": 1,\n", - " },\n", - " },\n", - " output={\"name\": OUTPUT_NAME},\n", - ")\n", - "\n", - "# `rl` auto-generates the job id (rl-); do not pass name=.\n", - "job = sdk.customization.rl.jobs.create(spec=spec, workspace=\"default\")\n", - "print(f\"Job ID: {job.job.name}\")\n", - "print(f\"Output model: {OUTPUT_NAME}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 7. Track Training Progress\n", - "\n", - "The DPO job runs four steps: download -> **dpo-training** (Ray) -> upload -> model-entity. We poll the top-level job status and surface the training step's progress." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from IPython.display import clear_output\n", - "\n", - "check = max_wait_time_checker(7200, \"DPO job\")\n", - "while True:\n", - " check()\n", - " status = sdk.jobs.get_status(name=job.job.name, workspace=\"default\")\n", - " clear_output(wait=True)\n", - " print(f\"Job Status: {status.status}\")\n", - "\n", - " step = max_steps = phase = None\n", - " for job_step in status.steps or []:\n", - " if job_step.name == \"dpo-training\":\n", - " for task in job_step.tasks or []:\n", - " d = task.status_details or {}\n", - " step, max_steps, phase = d.get(\"step\"), d.get(\"max_steps\"), d.get(\"phase\")\n", - " break\n", - " break\n", - " if step is not None and max_steps:\n", - " print(f\"Training: Step {step}/{max_steps} ({100 * step / max_steps:.1f}%)\")\n", - " if phase:\n", - " print(f\"Phase: {phase}\")\n", - "\n", - " if status.status in (\"completed\", \"failed\", \"cancelled\", \"error\"):\n", - " print(f\"\\nJob finished: {status.status}\")\n", - " break\n", - " time.sleep(15)\n", - "\n", - "assert status.status == \"completed\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Interpreting DPO training metrics** (in `status_details.metrics`):\n", - "\n", - "- **`loss`** — the DPO loss; should trend down as the policy learns to separate chosen from rejected.\n", - "- **Reward margin** (chosen minus rejected reward) — should trend **up**: the model increasingly prefers chosen responses.\n", - "- **Validation `loss`** — watch for divergence from training loss (overfitting). Raise `ref_policy_kl_penalty` (β) or add `sft_loss_weight` if the policy drifts too far from the reference." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 8. Validate the Output Model\n", - "\n", - "DPO produces a **full-weight model entity** (not an adapter). Confirm it was registered." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "model_entity = sdk.models.retrieve(workspace=\"default\", name=OUTPUT_NAME)\n", - "print(model_entity.model_dump_json(indent=2))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 9. Deploy and Evaluate (optional)\n", - "\n", - "The DPO output is a full model, so it deploys like any full-weight checkpoint (see the [Full SFT](./sft-customization-job) tutorial for details). We deploy with vLLM and send a chat completion." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "deploy_suffix = uuid.uuid4().hex[:8]\n", - "DEPLOYMENT_CONFIG_NAME = f\"dpo-deployment-cfg-{deploy_suffix}\"\n", - "DEPLOYMENT_NAME = f\"dpo-deployment-{deploy_suffix}\"\n", - "\n", - "deployment_config = sdk.inference.deployment_configs.create(\n", - " workspace=\"default\",\n", - " name=DEPLOYMENT_CONFIG_NAME,\n", - " engine=\"vllm\",\n", - " model_spec={\"model_namespace\": \"default\", \"model_name\": OUTPUT_NAME},\n", - " executor_config={\"gpu\": 1, \"image_name\": \"vllm/vllm-openai\", \"image_tag\": \"v0.22.1\"},\n", - ")\n", - "\n", - "deployment = sdk.inference.deployments.create(\n", - " workspace=\"default\", name=DEPLOYMENT_NAME, config=deployment_config.name\n", - ")\n", - "print(f\"Deployment name: {deployment.name}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "check = max_wait_time_checker(1800, \"Deployment\")\n", - "while True:\n", - " check()\n", - " deployment_status = sdk.inference.deployments.retrieve(name=deployment.name, workspace=\"default\")\n", - " clear_output(wait=True)\n", - " print(f\"Deployment status: {deployment_status.status}\")\n", - " if str(deployment_status.status).lower() in (\"ready\", \"running\", \"failed\", \"error\"):\n", - " break\n", - " time.sleep(15)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "messages = [\n", - " {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n", - " {\"role\": \"user\", \"content\": \"Write a short, friendly email to a colleague asking to reschedule our meeting to Thursday.\"},\n", - "]\n", - "\n", - "response = sdk.inference.gateway.provider.post(\n", - " \"v1/chat/completions\",\n", - " name=deployment.name,\n", - " workspace=\"default\",\n", - " body={\"model\": f\"default/{OUTPUT_NAME}\", \"messages\": messages, \"temperature\": 0.7, \"max_tokens\": 256},\n", - ")\n", - "print(\"Model output:\\n\")\n", - "print(response[\"choices\"][0][\"message\"][\"content\"])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Conclusion\n", - "\n", - "You aligned a base model with **DPO** on the NeMo Platform using the `rl` backend:\n", - "\n", - "- Uploaded a HelpSteer3 preference dataset **as-is** (the platform detects the schema natively).\n", - "- Submitted a full-weight DPO job that ran on a Ray cluster via the Kubernetes executor.\n", - "- Registered the output as a full model entity and (optionally) deployed it for inference.\n", - "\n", - "**Next steps:** tune the alignment strength with `ref_policy_kl_penalty` (β), add `sft_loss_weight` to anchor the policy to the chosen responses, enable `activation_checkpointing` for memory headroom, or scale up with `parallelism`. See the `nemo-customizer` skill's `references/hyperparameters.md` (section NeMo-RL (DPO)) for the full knob reference." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.14" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/docs/customizer/tutorials/embedding-customization-job.ipynb b/docs/customizer/tutorials/embedding-customization-job.ipynb index ee2332e9f4..b552332593 100644 --- a/docs/customizer/tutorials/embedding-customization-job.ipynb +++ b/docs/customizer/tutorials/embedding-customization-job.ipynb @@ -50,8 +50,10 @@ "\n", "1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n", "2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n", - "3. **HuggingFace token** with read access to download the SPECTER dataset (get one at [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens))\n", - "4. **NGC API key** to pull NIM container images from nvcr.io (get one at [ngc.nvidia.com](https://ngc.nvidia.com/) → Setup → Generate API Key)" + "3. **NGC API key** to pull NIM container images from nvcr.io (get one at [ngc.nvidia.com](https://ngc.nvidia.com/) → Setup → Generate API Key)\n", + "4. **At least one GPU with CUDA 13+**\n", + "\n", + "The SPECTER dataset and the tutorial's base model are public and do not require a Hugging Face token. If you substitute a gated or private model, provide a token with read access." ] }, { @@ -312,7 +314,7 @@ "source": [ "### 3. Prepare Dataset\n", "\n", - "Use the [SPECTER dataset](https://huggingface.co/datasets/embedding-data/SPECTER) from HuggingFace, a collection of scientific paper triplets where papers that cite each other are considered related.\n", + "Use the [SPECTER dataset](https://huggingface.co/datasets/embedding-data/SPECTER) from Hugging Face, a collection of scientific paper triplets where papers that cite each other are considered related.\n", "\n", "**Dataset structure:**\n", "- ~684K scientific paper triplets (this tutorial uses 10%)\n", @@ -344,12 +346,6 @@ "from datasets import load_dataset\n", "import json\n", "\n", - "# HuggingFace token for dataset access\n", - "HF_TOKEN = os.environ.get(\"HF_TOKEN\")\n", - "if not HF_TOKEN:\n", - " raise ValueError(\"HF_TOKEN environment variable is required. Get one at https://huggingface.co/settings/tokens\")\n", - "os.environ[\"HF_TOKEN\"] = HF_TOKEN\n", - "\n", "# Configuration\n", "DATASET_SIZE = 3000 # Number of triplets (increase for better results, max ~684K)\n", "VALIDATION_SPLIT = 0.05 # 5% held out for validation\n", @@ -439,11 +435,11 @@ "Configure authentication for accessing base models:\n", "\n", "- **NGC models** (`ngc://` URIs): Requires NGC API key\n", - "- **HuggingFace models** (`hf://` URIs): Requires HF token for gated/private models\n", + "- **Hugging Face models** (`hf://` URIs): Requires HF token for gated/private models\n", "\n", "Get your credentials:\n", "- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n", - "- [HuggingFace Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n", + "- [Hugging Face Token](https://huggingface.co/settings/tokens) (Optional; needed only for a gated/private replacement model)\n", "\n", "---\n", "\n", @@ -477,9 +473,10 @@ " return client.secrets.retrieve(name=name, workspace=\"default\")\n", "\n", "\n", - "# Create HuggingFace token secret (for downloading model from HF during training)\n", - "hf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\n", - "print(f\"HF_TOKEN secret: {hf_secret.name}\")\n", + "# Public Hugging Face models need no token. Create a secret only when HF_TOKEN is set.\n", + "hf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\") if HF_TOKEN else None\n", + "if hf_secret:\n", + " print(f\"HF_TOKEN secret: {hf_secret.name}\")\n", "\n", "# NGC secret was already created in baseline step (Step 2), or use the platform default\n", "if \"NGC_SECRET_NAME\" not in globals():\n", @@ -495,7 +492,7 @@ "source": [ "### 7. Create Base Model FileSet and Model Entity\n", "\n", - "Create a fileset pointing to the [nvidia/llama-nemotron-embed-1b-v2](https://huggingface.co/nvidia/llama-nemotron-embed-1b-v2) embedding model from HuggingFace, then create a Model Entity that references this fileset. Model downloading will take place at training time." + "Create a fileset pointing to the [nvidia/llama-nemotron-embed-1b-v2](https://huggingface.co/nvidia/llama-nemotron-embed-1b-v2) embedding model from Hugging Face, then create a Model Entity that references this fileset. Model downloading will take place at training time." ] }, { @@ -508,20 +505,21 @@ "HF_REPO_ID = \"nvidia/llama-nemotron-embed-1b-v2\"\n", "MODEL_NAME = \"nv-nemotron-embed-1b-base\"\n", "\n", - "# Ensure you have a HuggingFace token secret created\n", + "storage_kwargs = {\n", + " \"type\": \"huggingface\",\n", + " \"repo_id\": HF_REPO_ID,\n", + " \"repo_type\": \"model\",\n", + "}\n", + "if hf_secret:\n", + " storage_kwargs[\"token_secret\"] = hf_secret.name\n", + "storage = HuggingfaceStorageConfigParam(**storage_kwargs)\n", + "\n", "try:\n", " base_model_fs = client.files.filesets.create(\n", " workspace=\"default\",\n", " name=MODEL_NAME,\n", " description=\"NVIDIA Llama Nemotron Embed 1B v2 embedding model\",\n", - " storage=HuggingfaceStorageConfigParam(\n", - " type=\"huggingface\",\n", - " # repo_id is the full model name from Hugging Face\n", - " repo_id=HF_REPO_ID,\n", - " repo_type=\"model\",\n", - " # we use the secret created in the previous step\n", - " token_secret=hf_secret.name\n", - " )\n", + " storage=storage,\n", " )\n", "except ConflictError as e:\n", " print(f\"Base model fileset already exists. Skipping creation.\")\n", @@ -595,7 +593,7 @@ " \"training_type\": \"sft\",\n", " \"finetuning_type\": \"lora_merged\",\n", " \"lora\": {\"rank\": 16, \"alpha\": 32},\n", - " \"max_seq_length\": MAX_SEQ_LENGTH,\n", + " \"max_seq_length\": 512,\n", "}\n", "```\n" ] @@ -693,7 +691,10 @@ " print(f\"\\nJob finished with status: {status.status}\")\n", " break\n", " \n", - " time.sleep(10)" + " time.sleep(10)\n", + "\n", + "if status.status != \"completed\":\n", + " raise RuntimeError(f\"Training job finished with status: {status.status}\")" ], "execution_count": null, "outputs": [] @@ -918,7 +919,7 @@ "\n", "**Benchmark Evaluation**\n", "\n", - "For systematic evaluation, use the NeMo Evaluator service with retrieval benchmarks like SciDocs, BEIR, or MTEB. Refer to the [Evaluator documentation](../../evaluator/index.md) for details.\n", + "For systematic evaluation of end-to-end retrieval quality in a RAG pipeline, use the NeMo Evaluator [RAG metrics](../../evaluator/metrics/rag.md) (RAGAS `context_recall`, `context_precision`, and `context_relevance`).\n", "\n", "---\n", "\n", @@ -930,10 +931,10 @@ "\n", "| Parameter | Recommended | Notes |\n", "|-----------|-------------|-------|\n", - "| `learning_rate` | 1e-6 to 5e-6 | Lower than standard SFT |\n", - "| `batch_size` | 128-256 | Larger batches improve contrastive learning |\n", - "| `max_seq_length` | 512 | Typical for embedding models |\n", - "| `epochs` | 1-3 | Start small, increase if needed |\n", + "| `optimizer.learning_rate` | 1e-6 to 5e-6 | Lower than standard SFT |\n", + "| `batch.global_batch_size` | 128-256 | Larger batches improve contrastive learning |\n", + "| `training.max_seq_length` | 512 | Typical for embedding models |\n", + "| `schedule.epochs` | 1-3 | Start small, increase if needed |\n", "\n", "---\n", "\n", @@ -959,7 +960,7 @@ "## Next Steps\n", "\n", "- [Monitor training metrics](../manage-customization-jobs/get-job-status.md) in detail\n", - "- [Evaluate your model](../../evaluator/index.md) with retrieval benchmarks\n", + "- [Evaluate your model](../../evaluator/metrics/rag.md) with RAG metrics\n", "- Integrate the fine-tuned embedding model into your RAG pipeline\n", "- Scale up training with the full SPECTER dataset (~684K triplets) for better results" ] @@ -986,4 +987,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/customizer/tutorials/embedding-customization-job.mdx b/docs/customizer/tutorials/embedding-customization-job.mdx index 222dbf3ccb..41151674d8 100644 --- a/docs/customizer/tutorials/embedding-customization-job.mdx +++ b/docs/customizer/tutorials/embedding-customization-job.mdx @@ -5,8 +5,6 @@ description: "" [Run in Google Colab](https://colab.research.google.com/github/NVIDIA-NeMo/nemo-platform/blob/main/docs/customizer/tutorials/embedding-customization-job.ipynb) -# Embedding Model Customization - Learn how to fine-tune an embedding model to improve retrieval accuracy for your specific domain. ## About @@ -43,8 +41,10 @@ Before starting this tutorial, ensure you have: 1. **Completed the [Quickstart](/documentation/get-started)** to install and deploy NeMo Platform locally 2. **Installed the Python SDK** (PyPI wrapper: `pip install "nemo-platform[all]"`; source checkout: run `make bootstrap` from the repository root) -3. **HuggingFace token** with read access to download the SPECTER dataset (get one at [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)) -4. **NGC API key** to pull NIM container images from nvcr.io (get one at [ngc.nvidia.com](https://ngc.nvidia.com/) → Setup → Generate API Key) +3. **NGC API key** to pull NIM container images from nvcr.io (get one at [ngc.nvidia.com](https://ngc.nvidia.com/) → Setup → Generate API Key) +4. **At least one GPU with CUDA 13+** + +The SPECTER dataset and the tutorial's base model are public and do not require a Hugging Face token. If you substitute a gated or private model, provide a token with read access. ## Quick Start @@ -256,7 +256,7 @@ print("GPU freed. Proceed to fine-tune and improve these rankings.") ### 3. Prepare Dataset -Use the [SPECTER dataset](https://huggingface.co/datasets/embedding-data/SPECTER) from HuggingFace, a collection of scientific paper triplets where papers that cite each other are considered related. +Use the [SPECTER dataset](https://huggingface.co/datasets/embedding-data/SPECTER) from Hugging Face, a collection of scientific paper triplets where papers that cite each other are considered related. **Dataset structure:** - ~684K scientific paper triplets (this tutorial uses 10%) @@ -279,12 +279,6 @@ from pathlib import Path from datasets import load_dataset import json -# HuggingFace token for dataset access -HF_TOKEN = os.environ.get("HF_TOKEN") -if not HF_TOKEN: - raise ValueError("HF_TOKEN environment variable is required. Get one at https://huggingface.co/settings/tokens") -os.environ["HF_TOKEN"] = HF_TOKEN - # Configuration DATASET_SIZE = 3000 # Number of triplets (increase for better results, max ~684K) VALIDATION_SPLIT = 0.05 # 5% held out for validation @@ -358,11 +352,11 @@ print(json.dumps([f.model_dump() for f in client.files.list(fileset=DATASET_NAME Configure authentication for accessing base models: - **NGC models** (`ngc://` URIs): Requires NGC API key -- **HuggingFace models** (`hf://` URIs): Requires HF token for gated/private models +- **Hugging Face models** (`hf://` URIs): Requires HF token for gated/private models Get your credentials: - [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key) -- [HuggingFace Token](https://huggingface.co/settings/tokens) (Create token with Read access) +- [Hugging Face Token](https://huggingface.co/settings/tokens) (Optional; needed only for a gated/private replacement model) --- @@ -392,9 +386,10 @@ def create_or_get_secret(name: str, value: str | None, label: str): return client.secrets.retrieve(name=name, workspace="default") -# Create HuggingFace token secret (for downloading model from HF during training) -hf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN") -print(f"HF_TOKEN secret: {hf_secret.name}") +# Public Hugging Face models need no token. Create a secret only when HF_TOKEN is set. +hf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN") if HF_TOKEN else None +if hf_secret: + print(f"HF_TOKEN secret: {hf_secret.name}") # NGC secret was already created in baseline step (Step 2), or use the platform default if "NGC_SECRET_NAME" not in globals(): @@ -404,7 +399,7 @@ print(f"NGC_API_KEY secret: {NGC_SECRET_NAME}") ### 7. Create Base Model FileSet and Model Entity -Create a fileset pointing to the [nvidia/llama-nemotron-embed-1b-v2](https://huggingface.co/nvidia/llama-nemotron-embed-1b-v2) embedding model from HuggingFace, then create a Model Entity that references this fileset. Model downloading will take place at training time. +Create a fileset pointing to the [nvidia/llama-nemotron-embed-1b-v2](https://huggingface.co/nvidia/llama-nemotron-embed-1b-v2) embedding model from Hugging Face, then create a Model Entity that references this fileset. Model downloading will take place at training time. ```python import time @@ -413,20 +408,21 @@ from nemo_platform.types.files import HuggingfaceStorageConfigParam HF_REPO_ID = "nvidia/llama-nemotron-embed-1b-v2" MODEL_NAME = "nv-nemotron-embed-1b-base" -# Ensure you have a HuggingFace token secret created +storage_kwargs = { + "type": "huggingface", + "repo_id": HF_REPO_ID, + "repo_type": "model", +} +if hf_secret: + storage_kwargs["token_secret"] = hf_secret.name +storage = HuggingfaceStorageConfigParam(**storage_kwargs) + try: base_model_fs = client.files.filesets.create( workspace="default", name=MODEL_NAME, description="NVIDIA Llama Nemotron Embed 1B v2 embedding model", - storage=HuggingfaceStorageConfigParam( - type="huggingface", - # repo_id is the full model name from Hugging Face - repo_id=HF_REPO_ID, - repo_type="model", - # we use the secret created in the previous step - token_secret=hf_secret.name - ) + storage=storage, ) except ConflictError as e: print(f"Base model fileset already exists. Skipping creation.") @@ -494,7 +490,7 @@ training={ "training_type": "sft", "finetuning_type": "lora_merged", "lora": {"rank": 16, "alpha": 32}, - "max_seq_length": MAX_SEQ_LENGTH, + "max_seq_length": 512, } ``` @@ -580,6 +576,9 @@ while True: break time.sleep(10) + +if status.status != "completed": + raise RuntimeError(f"Training job finished with status: {status.status}") ``` **Interpreting Embedding Training Metrics:** @@ -770,10 +769,10 @@ For detailed information on all available hyperparameters, recommended values, a | Parameter | Recommended | Notes | |-----------|-------------|-------| -| `learning_rate` | 1e-6 to 5e-6 | Lower than standard SFT | -| `batch_size` | 128-256 | Larger batches improve contrastive learning | -| `max_seq_length` | 512 | Typical for embedding models | -| `epochs` | 1-3 | Start small, increase if needed | +| `optimizer.learning_rate` | 1e-6 to 5e-6 | Lower than standard SFT | +| `batch.global_batch_size` | 128-256 | Larger batches improve contrastive learning | +| `training.max_seq_length` | 512 | Typical for embedding models | +| `schedule.epochs` | 1-3 | Start small, increase if needed | --- diff --git a/docs/customizer/tutorials/format-training-dataset.mdx b/docs/customizer/tutorials/format-training-dataset.mdx index b08d157467..c16b236123 100644 --- a/docs/customizer/tutorials/format-training-dataset.mdx +++ b/docs/customizer/tutorials/format-training-dataset.mdx @@ -288,7 +288,7 @@ if model.spec: print(f"Is Chat Model: {model.spec.is_chat}") print(f"Family: {model.spec.family}") print(f"Parameters: {model.spec.base_num_parameters:,}") - print(f"Max Sequence Length: {model.spec.max_sequence_length}") + print(f"Context Size: {model.spec.context_size}") ``` ### Chat with the Model @@ -330,13 +330,6 @@ print(f"Response: {response.choices[0].message.content}") ```` -## Next Steps - -Now that you know how to format your training datasets, you can proceed with creating customization jobs: - -- [Start a LoRA Model Customization Job](./lora-customization-job.ipynb) - For parameter-efficient fine-tuning -- [Start a Full SFT Customization Job](./sft-customization-job.ipynb) - For full model fine-tuning - --- ## Completion Models @@ -382,3 +375,10 @@ response = oai_client.completions.create( print(f"Response: {response.choices[0].text}") ```` + +## Next Steps + +Now that you know how to format your training datasets, you can proceed with creating customization jobs: + +- [Start a LoRA Model Customization Job](/documentation/customizer-reference/tutorials/lora-customization-job) - For parameter-efficient fine-tuning +- [Start a Full SFT Customization Job](/documentation/customizer-reference/tutorials/sft-customization-job) - For full model fine-tuning diff --git a/docs/customizer/tutorials/import-hf-model.mdx b/docs/customizer/tutorials/import-hf-model.mdx index 8160f4a531..b75bf1be5f 100644 --- a/docs/customizer/tutorials/import-hf-model.mdx +++ b/docs/customizer/tutorials/import-hf-model.mdx @@ -1,10 +1,10 @@ --- -title: "Import and Fine-Tune Private HuggingFace Models" +title: "Import and Fine-Tune Private Hugging Face Models" description: "" --- -Use this tutorial to learn how to import a private HuggingFace model into NeMo Customizer, fine-tune it with LoRA, and deploy it for inference. +Use this tutorial to learn how to import a private Hugging Face model into NeMo Customizer, fine-tune it with LoRA, and deploy it for inference. ## Prerequisites @@ -14,8 +14,8 @@ Use this tutorial to learn how to import a private HuggingFace model into NeMo C - Completed the [Quickstart](/documentation/get-started) to install and deploy NeMo Platform locally. - Installed the Python SDK and any tutorial packages you need in your environment. -- A HuggingFace token with access to the private or gated model repository. -- A HuggingFace model with a compatible architecture. This tutorial uses `google/gemma-2-2b-it` as an example, but success depends on architectural compatibility. +- A Hugging Face token with access to the private or gated model repository. +- A Hugging Face model with a compatible architecture. This tutorial uses `google/gemma-2-2b-it` as an example, but success depends on architectural compatibility. - Sufficient GPU memory for the model and LoRA training job. @@ -100,9 +100,9 @@ NMP_BASE_URL = os.environ.get("NMP_BASE_URL", "http://localhost:8080") client = NeMoPlatform(base_url=NMP_BASE_URL, workspace="default") ``` -### 2. Store the HuggingFace Token +### 2. Store the Hugging Face Token -Private and gated HuggingFace repositories require a token. Store it as a NeMo Platform secret and reference that secret from the HuggingFace fileset. +Private and gated Hugging Face repositories require a token. Store it as a NeMo Platform secret and reference that secret from the Hugging Face fileset. ```python def create_or_get_secret(name: str, value: str | None, label: str) -> PlatformSecretResponse: @@ -126,7 +126,7 @@ hf_secret = create_or_get_secret("hf-token", os.getenv("HF_TOKEN"), "HF_TOKEN") ### 3. Create a Model FileSet and Model Entity -Create a HuggingFace-backed fileset for the private model, then register a Model Entity that points to that fileset. Model files are downloaded by the platform when training or deployment needs them. +Create a Hugging Face-backed fileset for the private model, then register a Model Entity that points to that fileset. Model files are downloaded by the platform when training or deployment needs them. ```python HF_REPO_ID = "google/gemma-2-2b-it" @@ -142,18 +142,18 @@ try: base_model_fs = client.files.filesets.create( workspace="default", name=MODEL_NAME, - description=f"Private HuggingFace model {HF_REPO_ID}", + description=f"Private Hugging Face model {HF_REPO_ID}", storage=model_storage, cache=True, ) print(f"Created model fileset: {base_model_fs.name}") except ConflictError: - print(f"Model fileset '{MODEL_NAME}' already exists, refreshing HuggingFace settings...") + print(f"Model fileset '{MODEL_NAME}' already exists, refreshing Hugging Face settings...") client.files.filesets.delete(workspace="default", name=MODEL_NAME) base_model_fs = client.files.filesets.create( workspace="default", name=MODEL_NAME, - description=f"Private HuggingFace model {HF_REPO_ID}", + description=f"Private Hugging Face model {HF_REPO_ID}", storage=model_storage, cache=True, ) @@ -258,7 +258,7 @@ try: client.files.filesets.create( workspace="default", name=DATASET_NAME, - description="Private HuggingFace model LoRA training data", + description="Private Hugging Face model LoRA training data", cache=True, ) print(f"Created dataset fileset: {DATASET_NAME}") @@ -442,8 +442,10 @@ def chat(model_id: str): ) -base_response = chat(f"default/{MODEL_NAME}") -lora_response = chat(f"default--{OUTPUT_NAME}") +BASE_INFERENCE_MODEL_NAME = f"default/{MODEL_NAME}" +INFERENCE_MODEL_NAME = f"default--{OUTPUT_NAME}" +base_response = chat(BASE_INFERENCE_MODEL_NAME) +lora_response = chat(INFERENCE_MODEL_NAME) print("Base model response:") print(base_response["choices"][0]["message"]["content"]) @@ -457,11 +459,12 @@ print(lora_response["choices"][0]["message"]["content"]) ```bash export OUTPUT_NAME="" +export INFERENCE_MODEL_NAME="default--${OUTPUT_NAME}" curl -s "${NMP_BASE_URL}/apis/inference-gateway/v2/workspaces/default/openai/-/v1/chat/completions" \ -H 'Content-Type: application/json' \ -d '{ - "model": "'${OUTPUT_NAME}'", + "model": "'${INFERENCE_MODEL_NAME}'", "messages": [ {"role": "user", "content": "Can you summarize what LoRA fine-tuning does?"} ], diff --git a/docs/customizer/tutorials/lora-customization-job.ipynb b/docs/customizer/tutorials/lora-customization-job.ipynb index 6bb42e9947..cf8f0667b0 100644 --- a/docs/customizer/tutorials/lora-customization-job.ipynb +++ b/docs/customizer/tutorials/lora-customization-job.ipynb @@ -27,7 +27,8 @@ "1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n", "2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n", "3. **Installed the `datasets` package** for loading SQuAD: `pip install datasets`\n", - "4. **At least one GPU with CUDA 12.8+**\n" + "4. **At least one GPU with CUDA 13+**\n", + "" ] }, { @@ -213,9 +214,9 @@ "source": [ "### 4. Secrets Setup\n", "\n", - "For Huggingface models that require authentication, create a secret with your HF token. Get a token from [Huggingface Settings](https://huggingface.co/settings/tokens) and accept the model terms.\n", + "For Hugging Face models that require authentication, create a secret with your HF token. Get a token from [Hugging Face Settings](https://huggingface.co/settings/tokens) and accept the model terms.\n", "\n", - "This is generally true for LLaMa based models (e.g. [Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)).\n", + "This is generally true for Llama-based models (for example, [Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)).\n", "\n", "```sh\n", "export HF_TOKEN=\n", @@ -276,7 +277,7 @@ " base_model_fs = client.files.filesets.create(\n", " workspace=\"default\",\n", " name=MODEL_NAME,\n", - " description=\"Qwen3 0.6b base model from Huggingface\",\n", + " description=\"Qwen3 0.6b base model from Hugging Face\",\n", " storage=storage,\n", " cache=True,\n", " )\n", @@ -517,12 +518,13 @@ "messages = [\n", " {\"role\": \"user\", \"content\": f\"Based on the following context, answer the question.\\n\\nContext: {context}\\n\\nQuestion: {question}\"}\n", "]\n", + "INFERENCE_MODEL_NAME = f\"default--{OUTPUT_NAME}\"\n", "response = client.inference.gateway.provider.post(\n", " \"v1/chat/completions\",\n", " name=deployment_name,\n", " workspace=\"default\",\n", " body={\n", - " \"model\": OUTPUT_NAME,\n", + " \"model\": INFERENCE_MODEL_NAME,\n", " \"messages\": messages,\n", " \"temperature\": 0,\n", " \"max_tokens\": 256,\n", @@ -550,8 +552,7 @@ "\n", "- [Monitor training metrics](fine-tune-metrics) in detail\n", "- [Evaluate your fine-tuned model](../../evaluator/index) using the Evaluator service\n", - "- Try [Full SFT](./sft-customization-job) for other customization options\n", - "" + "- Try [Full SFT](./sft-customization-job) for other customization options\n" ] } ], diff --git a/docs/customizer/tutorials/lora-customization-job.mdx b/docs/customizer/tutorials/lora-customization-job.mdx index d7434b20a7..4cf7f208fa 100644 --- a/docs/customizer/tutorials/lora-customization-job.mdx +++ b/docs/customizer/tutorials/lora-customization-job.mdx @@ -5,8 +5,6 @@ description: "" [Run in Google Colab](https://colab.research.google.com/github/NVIDIA-NeMo/nemo-platform/blob/main/docs/customizer/tutorials/lora-customization-job.ipynb) -# LoRA Model Customization Job - Learn how to use the NeMo Platform to create a LoRA (Low-Rank Adaptation) customization job using a custom dataset. In this tutorial we use LoRA to fine-tune a **question-answering model** from the SQuAD dataset. LoRA is a parameter-efficient fine-tuning method that requires fewer computational resources than full fine-tuning. If you need full model fine-tuning instead, see the [Full SFT Customization Job](/documentation/customizer-reference/tutorials/sft-customization-job) tutorial. @@ -20,7 +18,7 @@ Before starting this tutorial, ensure you have: 1. **Completed the [Quickstart](/documentation/get-started)** to install and deploy NeMo Platform locally 2. **Installed the Python SDK** (PyPI wrapper: `pip install "nemo-platform[all]"`; source checkout: run `make bootstrap` from the repository root) 3. **Installed the `datasets` package** for loading SQuAD: `pip install datasets` -4. **At least one GPU with CUDA 12.8+** +4. **At least one GPU with CUDA 13+** ## Quick Start @@ -164,9 +162,9 @@ print(client.files.list(fileset=DATASET_NAME, workspace="default")) ### 4. Secrets Setup -For Huggingface models that require authentication, create a secret with your HF token. Get a token from [Huggingface Settings](https://huggingface.co/settings/tokens) and accept the model terms. +For Hugging Face models that require authentication, create a secret with your HF token. Get a token from [Hugging Face Settings](https://huggingface.co/settings/tokens) and accept the model terms. -This is generally true for LLaMa based models (e.g. [Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)). +This is generally true for Llama-based models (for example, [Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)). ```sh export HF_TOKEN= @@ -213,7 +211,7 @@ try: base_model_fs = client.files.filesets.create( workspace="default", name=MODEL_NAME, - description="Qwen3 0.6b base model from Huggingface", + description="Qwen3 0.6b base model from Hugging Face", storage=storage, cache=True, ) @@ -408,12 +406,13 @@ question = "Who was the first person to walk on the Moon?" messages = [ {"role": "user", "content": f"Based on the following context, answer the question.\n\nContext: {context}\n\nQuestion: {question}"} ] +INFERENCE_MODEL_NAME = f"default--{OUTPUT_NAME}" response = client.inference.gateway.provider.post( "v1/chat/completions", name=deployment_name, workspace="default", body={ - "model": OUTPUT_NAME, + "model": INFERENCE_MODEL_NAME, "messages": messages, "temperature": 0, "max_tokens": 256, diff --git a/docs/customizer/tutorials/metrics.mdx b/docs/customizer/tutorials/metrics.mdx index b857a39caa..a7794d62ca 100644 --- a/docs/customizer/tutorials/metrics.mdx +++ b/docs/customizer/tutorials/metrics.mdx @@ -22,8 +22,8 @@ The time to complete this tutorial is approximately 10 minutes. ### Tutorial-Specific Prerequisites -- Completed customization job with a valid ID -- (Optional) Access to NeMo with MLflow tracking enabled +- Completed customization job with a valid job name +- (Optional) A job created with `spec.integrations.mlflow` and access to its configured MLflow tracking server ## Available Metrics @@ -62,7 +62,7 @@ for step in status.steps or []: print(f"Training Phase: {details.get('phase')}") print(f"Step: {details.get('step')}/{details.get('max_steps')}") print(f"Epoch: {details.get('epoch')}/{details.get('num_epochs')}") - print(f"Training Loss: {details.get('loss')}") + print(f"Training Loss: {details.get('train_loss')}") print(f"Validation Loss: {details.get('val_loss')}") print(f"Learning Rate: {details.get('lr')}") print(f"Gradient Norm: {details.get('grad_norm')}") @@ -72,16 +72,16 @@ The response includes training progress and metrics including loss, learning rat ### Using MLflow -If your deployment has MLflow tracking enabled: +If your customization job was created with an `integrations.mlflow` configuration (see [MLflow Integration](/documentation/customizer-reference/manage-customization-jobs/customization-job-reference#mlflow-integration)): -1. Access the MLflow UI at your cluster's MLflow tracking URL -2. Locate your experiment by the output model name -3. Find the run using your customization job ID +1. Access the MLflow UI at the configured `tracking_uri` +2. Locate the configured `experiment_name` (defaults to the output model name) +3. Find the configured run `name` (defaults to the customization job ID) 4. View detailed metrics, including training and validation loss curves, under the "Metrics" tab -MLflow integration is configured at the cluster level. Contact your administrator if you need access to the MLflow UI or if MLflow tracking is not enabled for your deployment. +MLflow tracking is requested per job through `spec.integrations.mlflow`; it is not automatically enabled for every job in a cluster. The tracking server can be selected with `tracking_uri` in the job spec or the platform-side `MLFLOW_TRACKING_URI` environment variable. Contact your administrator if you need access to that server. diff --git a/docs/customizer/tutorials/optimize-throughput.ipynb b/docs/customizer/tutorials/optimize-throughput.ipynb index a4b6171f91..9296ae4e47 100644 --- a/docs/customizer/tutorials/optimize-throughput.ipynb +++ b/docs/customizer/tutorials/optimize-throughput.ipynb @@ -1,973 +1,991 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "\n", - "\n", - "\n", - "# Optimize for Tokens/GPU Throughput\n", - "\n", - "## About\n", - "Learn how to use the NeMo Platform Customizer to create a [LoRA](nemo-ms-about-concepts-customization) (Low-Rank Adaptation) customization job optimized for higher tokens/GPU throughput and lower runtime. \n", - "\n", - "**In this tutorial, you will:**\n", - "1. Fine-tune [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) on the SQuAD dataset using LoRA, with [sequence packing](nemo-ms-about-concepts-customization) enabled for one run and disabled for another.\n", - "2. Compare training runtime, GPU utilization, and memory allocation between the two runs.\n", - "3. Verify that validation loss remains comparable, confirming that sequence packing improves throughput without sacrificing model quality.\n", - "\n", - "> **Note:** While this tutorial demonstrates sequence packing with LoRA, the optimization is also available for all_weights (full) SFT customization jobs." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Prerequisites\n", - "\n", - "Before starting this tutorial, ensure you have:\n", - "\n", - "1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n", - "2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Quick Start\n", - "\n", - "### 1. Initialize SDK\n", - "\n", - "The SDK needs to know your NeMo Platform server URL. By default, `http://localhost:8080` is used in accordance with the [Quickstart](../../get-started/quickstart.md) guide. If NeMo Platform is running at a custom location, you can override the URL by setting the `NMP_BASE_URL` environment variable:\n", - "\n", - "```sh\n", - "export NMP_BASE_URL=\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import json\n", - "import os\n", - "from nemo_platform import NeMoPlatform, ConflictError\n", - "\n", - "NMP_BASE_URL = os.environ.get(\"NMP_BASE_URL\", \"http://localhost:8080\")\n", - "client = NeMoPlatform(\n", - " base_url=NMP_BASE_URL,\n", - " workspace=\"default\"\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 2. Create Dataset FileSet and Upload Training Data" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Install additional dependencies if they are not installed in your Python environment.\n", - "\n", - "The cell below automatically detects your environment and uses:\n", - "- `uv pip install` if you're in a uv-managed virtual environment\n", - "- `pip install` otherwise\n", - "\n", - "Required packages:\n", - "- `datasets` - Download the public [rajpurkar/squad](https://huggingface.co/datasets/rajpurkar/squad) dataset\n", - "- `pandas` - Compare job results in table format\n", - "- `matplotlib` - Plot live training metrics (loss curves, GPU utilization)\n", - "- `nvidia-ml-py` - Collect GPU VRAM and compute utilization metrics during training" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "vscode": { - "languageId": "shellscript" + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "# Optimize for Tokens/GPU Throughput\n", + "\n", + "## About\n", + "Learn how to use the NeMo Platform Customizer to create a [LoRA](nemo-ms-about-concepts-customization) (Low-Rank Adaptation) customization job optimized for higher tokens/GPU throughput and lower runtime. \n", + "\n", + "**In this tutorial, you will:**\n", + "1. Fine-tune [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) on the SQuAD dataset using LoRA, with [sequence packing](nemo-ms-about-concepts-customization) enabled for one run and disabled for another.\n", + "2. Compare training runtime, GPU utilization, and memory allocation between the two runs.\n", + "3. Verify that validation loss remains comparable, confirming that sequence packing improves throughput without sacrificing model quality.\n", + "\n", + "> **Note:** While this tutorial demonstrates sequence packing with LoRA, the optimization is also available for all_weights (full) SFT customization jobs." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "Before starting this tutorial, ensure you have:\n", + "\n", + "1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n", + "2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n", + "3. **At least one GPU with CUDA 13+**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Quick Start\n", + "\n", + "### 1. Initialize SDK\n", + "\n", + "The SDK needs to know your NeMo Platform server URL. By default, `http://localhost:8080` is used in accordance with the [Quickstart](../../get-started/quickstart.md) guide. If NeMo Platform is running at a custom location, you can override the URL by setting the `NMP_BASE_URL` environment variable:\n", + "\n", + "```sh\n", + "export NMP_BASE_URL=\n", + "```" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import json\n", + "import os\n", + "from nemo_platform import NeMoPlatform, ConflictError\n", + "\n", + "NMP_BASE_URL = os.environ.get(\"NMP_BASE_URL\", \"http://localhost:8080\")\n", + "client = NeMoPlatform(\n", + " base_url=NMP_BASE_URL,\n", + " workspace=\"default\"\n", + ")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2. Create Dataset FileSet and Upload Training Data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Install additional dependencies if they are not installed in your Python environment.\n", + "\n", + "The cell below automatically detects your environment and uses:\n", + "- `uv pip install` if you're in a uv-managed virtual environment\n", + "- `pip install` otherwise\n", + "\n", + "Required packages:\n", + "- `datasets` - Download the public [rajpurkar/squad](https://huggingface.co/datasets/rajpurkar/squad) dataset\n", + "- `pandas` - Compare job results in table format\n", + "- `matplotlib` - Plot live training metrics (loss curves, GPU utilization)\n", + "- `nvidia-ml-py` - Collect GPU VRAM and compute utilization metrics during training" + ] + }, + { + "cell_type": "code", + "metadata": { + "vscode": { + "languageId": "shellscript" + } + }, + "source": [ + "if command -v uv >/dev/null 2>&1 && [ -n \"$VIRTUAL_ENV\" ]; then\n", + " uv pip install datasets pandas matplotlib nvidia-ml-py\n", + "else\n", + " pip install datasets pandas matplotlib nvidia-ml-py\n", + "fi" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Download rajpurkar/squad Dataset\n", + "\n", + "SQuAD (Stanford Question Answering Dataset) is a reading comprehension dataset consisting of questions posed on Wikipedia articles, where the answer is a segment of text from the corresponding passage." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import json\n", + "import os\n", + "from pathlib import Path\n", + "from datasets import load_dataset, Dataset, DatasetDict\n", + "\n", + "# Configuration\n", + "SEED = 1234\n", + "DATASET_NAME = \"sft-dataset\"\n", + "\n", + "# Convert SQuAD format to prompt/completion format and save to JSONL\n", + "def convert_squad_to_sft_format(example):\n", + " \"\"\"Convert SQuAD format to prompt/completion format for SFT training.\"\"\"\n", + " prompt = f\"Context: {example['context']} Question: {example['question']} Answer:\"\n", + " completion = example[\"answers\"][\"text\"][0] # Take the first answer\n", + " return {\"prompt\": prompt, \"completion\": completion}\n", + "\n", + "# Load the SQuAD dataset from Hugging Face\n", + "print(\"Loading dataset rajpurkar/squad\")\n", + "ds = load_dataset(\"rajpurkar/squad\")\n", + "if not isinstance(ds, DatasetDict):\n", + " raise ValueError(\"Dataset does not contain expected splits\")\n", + "\n", + "print(\"Loaded dataset\")\n", + "\n", + "# For the purpose of this tutorial, we'll use a subset of the dataset\n", + "# We use a reduced dataset size (3000 training/300 validation samples) to keep tutorial runtime manageable\n", + "# while still demonstrating the performance benefits of sequence packing. The larger the dataset,\n", + "# the better the model will perform but the longer the training will take.\n", + "training_size = 3000\n", + "validation_size = 300\n", + "DATASET_PATH = Path(DATASET_NAME).absolute()\n", + "\n", + "# Get training split and verify it's a Dataset (not IterableDataset)\n", + "train_dataset = ds[\"train\"]\n", + "validation_dataset = ds[\"validation\"]\n", + "assert isinstance(train_dataset, Dataset), \"Expected Dataset type\"\n", + "assert isinstance(validation_dataset, Dataset), \"Expected Dataset type\"\n", + "\n", + "# Select subsets and save to JSONL files\n", + "training_ds = train_dataset.select(range(training_size))\n", + "validation_ds = validation_dataset.select(range(validation_size))\n", + "\n", + "# Transform to SFT format (prompt/completion)\n", + "training_ds = training_ds.map(convert_squad_to_sft_format, remove_columns=training_ds.column_names)\n", + "validation_ds = validation_ds.map(convert_squad_to_sft_format, remove_columns=validation_ds.column_names)\n", + "\n", + "# Create directory if it doesn't exist\n", + "# Note: This will create a local 'sft-dataset/' directory with training.jsonl and validation.jsonl files\n", + "os.makedirs(DATASET_PATH, exist_ok=True)\n", + "\n", + "# Save subsets to JSONL files\n", + "training_ds.to_json(f\"{DATASET_PATH}/training.jsonl\")\n", + "validation_ds.to_json(f\"{DATASET_PATH}/validation.jsonl\")\n", + "\n", + "print(f\"Saved training.jsonl with {len(training_ds)} rows\")\n", + "print(f\"Saved validation.jsonl with {len(validation_ds)} rows\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Create fileset to store SFT training data\n", + "\n", + "try:\n", + " client.files.filesets.create(\n", + " workspace=\"default\",\n", + " name=DATASET_NAME,\n", + " description=\"SFT training data\"\n", + " )\n", + " print(f\"Created fileset: {DATASET_NAME}\")\n", + "except ConflictError:\n", + " print(f\"Fileset '{DATASET_NAME}' already exists, continuing...\")\n", + "\n", + "# Upload training data files individually to ensure correct structure\n", + "client.files.upload(\n", + " local_path=f\"{DATASET_PATH}/\", # Trailing slash uploads directory contents to fileset root\n", + " remote_path=\"\",\n", + " fileset=DATASET_NAME,\n", + " workspace=\"default\"\n", + ")\n", + "\n", + "# Validate training data is uploaded correctly\n", + "print(\"Training data:\")\n", + "print(json.dumps([f.model_dump() for f in client.files.list(fileset=DATASET_NAME, workspace=\"default\").data], indent=2))" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3. Secrets Setup\n", + "\n", + "If you plan to use NGC or Hugging Face models, you will need to configure authentication:\n", + "\n", + "- **NGC models** (`ngc://` URIs): Requires NGC API key\n", + "- **Hugging Face models** (`hf://` URIs): Requires HF token for gated/private models\n", + "\n", + "\n", + "Configure these as secrets in your platform. Refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md) for detailed instructions.\n", + "\n", + "Get your credentials to access base models:\n", + "- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n", + "- [Hugging Face Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n", + "\n", + "\n", + "---\n", + "\n", + "#### Quick Setup Example\n", + "\n", + "This tutorial uses the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from Hugging Face. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access.\n", + "\n", + "**Hugging Face Authentication:**\n", + "- For gated models (Llama, Gemma), you must provide a Hugging Face token via the `token_secret` parameter\n", + "- Get your token from [Hugging Face Settings](https://huggingface.co/settings/tokens) (requires Read access)\n", + "- Accept the model's terms on the Hugging Face model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main)\n", + "- For public models, you can omit the `token_secret` parameter when creating a fileset for the model in the next step." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\n", + "HF_TOKEN = os.getenv(\"HF_TOKEN\")\n", + "NGC_API_KEY = os.getenv(\"NGC_API_KEY\")\n", + "\n", + "\n", + "def create_or_get_secret(name: str, value: str | None, label: str):\n", + " if not value:\n", + " raise ValueError(f\"{label} environment variable is not set. Set it and try again.\")\n", + " try:\n", + " secret = client.secrets.create(\n", + " name=name,\n", + " workspace=\"default\",\n", + " value=value,\n", + " )\n", + " print(f\"Created secret: {name}\")\n", + " return secret\n", + " except ConflictError:\n", + " print(f\"Secret '{name}' already exists, continuing...\")\n", + " return client.secrets.retrieve(name=name, workspace=\"default\")\n", + "\n", + "\n", + "# Create Hugging Face token secret\n", + "hf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\n", + "print(\"HF_TOKEN secret:\")\n", + "print(hf_secret.model_dump_json(indent=2))\n", + "\n", + "# Create NGC API key secret\n", + "# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n", + "# ngc_api_key = create_or_get_secret(\"ngc-api-key\", NGC_API_KEY, \"NGC_API_KEY\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4. Create Base Model FileSet\n", + "\n", + "Create a fileset pointing to the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model on Hugging Face. This step creates a pointer to the model on Hugging Face and does not download it. The model is downloaded at job creation time.\n", + "\n", + "Note: for public models, you can omit the `token_secret` parameter when creating a model fileset." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import time\n", + "from nemo_platform.types.files import HuggingfaceStorageConfigParam\n", + "\n", + "HF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\n", + "MODEL_NAME = \"llama-3-2-1b-base\"\n", + "\n", + "# Ensure you have a Hugging Face token secret created\n", + "# Create a fileset pointing to the desired Hugging Face model\n", + "try:\n", + " base_model_fs = client.files.filesets.create(\n", + " workspace=\"default\",\n", + " name=MODEL_NAME,\n", + " description=\"Llama 3.2 1B base model from Hugging Face\",\n", + " storage=HuggingfaceStorageConfigParam(\n", + " type=\"huggingface\",\n", + " # repo_id is the full model name from Hugging Face\n", + " repo_id=HF_REPO_ID,\n", + " repo_type=\"model\",\n", + " # we use the secret created in the previous step\n", + " token_secret=hf_secret.name\n", + " )\n", + " )\n", + " print(f\"Created base model fileset: {MODEL_NAME}\")\n", + "except ConflictError:\n", + " print(f\"Base model fileset already exists. Skipping creation.\")\n", + " base_model_fs = client.files.filesets.retrieve(\n", + " workspace=\"default\",\n", + " name=MODEL_NAME,\n", + " )\n", + "\n", + "# Create the Model Entity representation.\n", + "try:\n", + " base_model = client.models.create(\n", + " workspace=\"default\",\n", + " name=MODEL_NAME,\n", + " fileset=f\"default/{MODEL_NAME}\",\n", + " )\n", + " print(f\"Created Model Entity: {MODEL_NAME}\")\n", + "except ConflictError:\n", + " print(f\"Base model already exists. Updating fileset if different.\")\n", + " base_model = client.models.update(\n", + " workspace=\"default\",\n", + " name=MODEL_NAME,\n", + " fileset=f\"default/{MODEL_NAME}\",\n", + " )\n", + "\n", + "print(f\"\\nBase model fileset: fileset://default/{base_model.name}\")\n", + "print(\"Base model fileset files list:\")\n", + "print(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace=\"default\").data], indent=2))\n", + "\n", + "# Wait for ModelSpec to be populated from the checkpoint\n", + "print(\"\\nWaiting for ModelSpec to be populated...\")\n", + "SPEC_TIMEOUT_SECONDS = 120\n", + "spec_start = time.time()\n", + "while not base_model.spec:\n", + " if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n", + " raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds\")\n", + " time.sleep(2)\n", + " base_model = client.models.retrieve(\n", + " workspace=\"default\",\n", + " name=MODEL_NAME,\n", + " )\n", + "\n", + "print(f\"ModelSpec populated: {base_model.spec}\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5. Create LoRA Job with Sequence Packing\n", + "Create a LoRA customization job with **sequence packing** enabled via `AutomodelJobInput` (`batch.sequence_packing=True`)." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import uuid\n", + "from nemo_automodel_plugin.schema import AutomodelJobInput\n", + "\n", + "SEQUENCE_PACKING_ENABLED = True\n", + "\n", + "job_suffix = uuid.uuid4().hex[:4]\n", + "JOB_NAME = f\"packing-job-{job_suffix}\"\n", + "PACK_OUTPUT_NAME = f\"packing-out-{job_suffix}\"\n", + "\n", + "spec = AutomodelJobInput(\n", + " model=f\"default/{base_model.name}\",\n", + " dataset={\"training\": f\"default/{DATASET_NAME}\"},\n", + " training={\n", + " \"training_type\": \"sft\",\n", + " \"finetuning_type\": \"lora\",\n", + " \"max_seq_length\": 4096,\n", + " },\n", + " schedule={\"epochs\": 1, \"val_check_interval\": 0.1},\n", + " batch={\n", + " \"global_batch_size\": 64,\n", + " \"micro_batch_size\": 1,\n", + " \"sequence_packing\": SEQUENCE_PACKING_ENABLED,\n", + " },\n", + " optimizer={\"learning_rate\": 5e-5},\n", + " parallelism={\"num_gpus_per_node\": 1},\n", + " output={\"name\": PACK_OUTPUT_NAME},\n", + ")\n", + "\n", + "job_with_sequence_packing = client.customization.automodel.jobs.create(\n", + " spec=spec, workspace=\"default\", name=JOB_NAME\n", + ")\n", + "\n", + "print(f\"Submitted job: {job_with_sequence_packing.job.name}\")\n", + "print(f\"Output adapter: {PACK_OUTPUT_NAME}\")\n" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 6. Track Fine-Tuning Progress\n", + "\n", + "A training job contains multiple steps: \n", + "- Model and dataset downloading\n", + "- Fine-tuning where LoRA adapter weights are trained\n", + "- Creating a fileset entry for the fine-tuned model\n", + "- Fine-tuned weights uploading\n", + "\n", + "The elapsed time printed below reflects progress of the entire job. We compare the time taken by the fine-tuning step for both jobs in the last section of this tutorial." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Define Helper Functions" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "\n", + "# Helpers to draw GPU VRAM Utilization and Validation Loss\n", + "import matplotlib.pyplot as plt\n", + "try:\n", + " import pynvml\n", + " _PYNVML_AVAILABLE = True\n", + "except ImportError:\n", + " _PYNVML_AVAILABLE = False\n", + " print(\"Note: Install nvidia-ml-py ('pip install nvidia-ml-py' or 'uv pip install nvidia-ml-py') to enable live GPU metrics.\")\n", + "\n", + "# ---------------------------------------------------------------------------\n", + "# GPU metrics collection (nvidia-ml-py; import name is pynvml)\n", + "# ---------------------------------------------------------------------------\n", + "\n", + "def _get_gpu_snapshot() -> tuple[list[float], list[float]]:\n", + " \"\"\"Return (vram_usage_pcts, compute_util_pcts) for each GPU.\"\"\"\n", + " if not _PYNVML_AVAILABLE:\n", + " return [], []\n", + " pynvml.nvmlInit()\n", + " try:\n", + " vram, util = [], []\n", + " for i in range(pynvml.nvmlDeviceGetCount()):\n", + " h = pynvml.nvmlDeviceGetHandleByIndex(i)\n", + " mem = pynvml.nvmlDeviceGetMemoryInfo(h)\n", + " rates = pynvml.nvmlDeviceGetUtilizationRates(h)\n", + " vram.append(int(mem.used) / int(mem.total) * 100)\n", + " util.append(float(rates.gpu))\n", + " return vram, util\n", + " finally:\n", + " pynvml.nvmlShutdown()\n", + "\n", + "\n", + "# ---------------------------------------------------------------------------\n", + "# Dashboard drawing helpers\n", + "# ---------------------------------------------------------------------------\n", + "\n", + "_PALETTE = {\n", + " \"val_loss\": \"#E74C3C\",\n", + " \"train_loss\": \"#F39C12\",\n", + " \"vram\": [\"#3498DB\", \"#9B59B6\", \"#1ABC9C\", \"#E67E22\"],\n", + " \"util\": [\"#2ECC71\", \"#E74C3C\", \"#3498DB\", \"#F1C40F\"],\n", + " \"grid\": \"#ECECEC\",\n", + " \"title\": \"#2C3E50\",\n", + " \"subtitle\": \"#7F8C8D\",\n", + " \"spine\": \"#CCCCCC\",\n", + " \"tick\": \"#666666\",\n", + "}\n", + "\n", + "\n", + "def _style_axis(ax):\n", + " \"\"\"Apply shared cosmetic styling to a subplot axis.\"\"\"\n", + " ax.set_facecolor(\"white\")\n", + " ax.grid(True, alpha=0.4, color=_PALETTE[\"grid\"], linewidth=0.8)\n", + " for spine in (\"top\", \"right\"):\n", + " ax.spines[spine].set_visible(False)\n", + " ax.spines[\"left\"].set_color(_PALETTE[\"spine\"])\n", + " ax.spines[\"bottom\"].set_color(_PALETTE[\"spine\"])\n", + " ax.tick_params(colors=_PALETTE[\"tick\"], labelsize=9)\n", + "\n", + "\n", + "def _plot_line(ax, xs, ys, color, label, fill=True):\n", + " \"\"\"Plot a time series, gracefully skipping None values.\"\"\"\n", + " pts = [(x, y) for x, y in zip(xs, ys) if y is not None]\n", + " if not pts:\n", + " return\n", + " px, py = zip(*pts)\n", + " ax.plot(\n", + " px, py, color=color, linewidth=2.2,\n", + " marker=\"o\", markersize=4,\n", + " markerfacecolor=\"white\", markeredgewidth=1.8, markeredgecolor=color,\n", + " label=label, zorder=3,\n", + " )\n", + " if fill:\n", + " ax.fill_between(px, py, alpha=0.08, color=color)\n", + "\n", + "\n", + "def _plot_gpu_panel(ax, xs, history, colors, fallback_label):\n", + " \"\"\"Plot per-GPU time series with area fill.\"\"\"\n", + " if not history or not history[0]:\n", + " ax.text(\n", + " 0.5, 0.5, \"No GPU data\", transform=ax.transAxes,\n", + " ha=\"center\", va=\"center\", fontsize=11, color=\"#AAAAAA\",\n", + " )\n", + " return\n", + " n_gpus = max(len(snap) for snap in history)\n", + " for g in range(n_gpus):\n", + " vals = [snap[g] if g < len(snap) else 0 for snap in history]\n", + " c = colors[g % len(colors)]\n", + " label = f\"GPU {g}\" if n_gpus > 1 else fallback_label\n", + " ax.plot(xs[: len(vals)], vals, color=c, linewidth=2, label=label)\n", + " ax.fill_between(xs[: len(vals)], vals, alpha=0.08, color=c)\n", + " if n_gpus > 1:\n", + " ax.legend(fontsize=9, framealpha=0.9, edgecolor=\"#DDD\")\n", + "\n", + "\n", + "def _draw_dashboard(\n", + " elapsed_mins, val_losses, train_losses,\n", + " vram_history, util_history,\n", + " job_name, status_str, step_str, elapsed_str,\n", + "):\n", + " \"\"\"Render a live 1x3 training dashboard.\"\"\"\n", + " fig, axes = plt.subplots(1, 3, figsize=(20, 5.5))\n", + " fig.patch.set_facecolor(\"#FAFBFC\")\n", + "\n", + " fig.suptitle(\n", + " job_name, fontsize=15, fontweight=\"bold\",\n", + " color=_PALETTE[\"title\"], y=1.10,\n", + " )\n", + " fig.text(\n", + " 0.5, 1.01,\n", + " f\"{status_str} | {step_str} | {elapsed_str}\",\n", + " ha=\"center\", fontsize=13, color=_PALETTE[\"subtitle\"],\n", + " )\n", + "\n", + " for ax in axes:\n", + " _style_axis(ax)\n", + "\n", + " # -- Panel 1: Loss curves --\n", + " _plot_line(axes[0], elapsed_mins, val_losses, _PALETTE[\"val_loss\"], \"Val Loss\", fill=True)\n", + " _plot_line(axes[0], elapsed_mins, train_losses, _PALETTE[\"train_loss\"], \"Train Loss\", fill=False)\n", + " axes[0].set_title(\"Train/Validation Loss\", fontsize=13, fontweight=\"bold\", color=_PALETTE[\"title\"], pad=12)\n", + " axes[0].set_xlabel(\"Time (min)\", fontsize=10, color=\"#666\")\n", + " axes[0].set_ylabel(\"Loss\", fontsize=10, color=\"#666\")\n", + " if any(v is not None for v in val_losses + train_losses):\n", + " axes[0].legend(fontsize=9, framealpha=0.9, edgecolor=\"#DDD\")\n", + "\n", + " # -- Panel 2: GPU VRAM usage --\n", + " _plot_gpu_panel(axes[1], elapsed_mins, vram_history, _PALETTE[\"vram\"], \"VRAM\")\n", + " axes[1].set_title(\"GPU VRAM Usage\", fontsize=13, fontweight=\"bold\", color=_PALETTE[\"title\"], pad=12)\n", + " axes[1].set_xlabel(\"Time (min)\", fontsize=10, color=\"#666\")\n", + " axes[1].set_ylabel(\"Usage (%)\", fontsize=10, color=\"#666\")\n", + " axes[1].set_ylim(-2, 105)\n", + "\n", + " # -- Panel 3: GPU utilization --\n", + " _plot_gpu_panel(axes[2], elapsed_mins, util_history, _PALETTE[\"util\"], \"Utilization\")\n", + " axes[2].set_title(\"GPU Utilization\", fontsize=13, fontweight=\"bold\", color=_PALETTE[\"title\"], pad=12)\n", + " axes[2].set_xlabel(\"Time (min)\", fontsize=10, color=\"#666\")\n", + " axes[2].set_ylabel(\"Utilization (%)\", fontsize=10, color=\"#666\")\n", + " axes[2].set_ylim(-2, 105)\n", + "\n", + " plt.tight_layout(rect=[0, 0, 1, 0.98])\n", + " plt.show()" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Monitor the Job Until Completion\n", + "\n", + "The cell below polls the job status every 10 seconds and renders a live dashboard with validation loss, GPU VRAM usage, and GPU utilization charts. The charts appear empty at first while the model and dataset download; training metrics and GPU activity populate after the fine-tuning step begins.\n", + "\n", + "> **Note:** This is additional code. You can also use the Weights & Biases or MLflow integrations." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import time\n", + "from typing import cast\n", + "from IPython.display import clear_output\n", + "from nemo_platform.types.shared import PlatformJobStatusResponse\n", + "\n", + "# Timeout set to 30 minutes to accommodate typical LoRA training duration for this dataset size.\n", + "# Actual training time will vary based on hardware, model size, and dataset complexity.\n", + "TIMEOUT_SECONDS = 30 * 60 # 30 minutes\n", + "VAL_LOSS_KEY = \"val_loss\"\n", + "TRAIN_LOSS_KEY = \"train_loss\"\n", + "\n", + "\n", + "def get_training_metric(\n", + " status: PlatformJobStatusResponse,\n", + " metric_key: str,\n", + ") -> float | None:\n", + " \"\"\"Return a metric reported by a task in the training step.\"\"\"\n", + " for job_step in status.steps or []:\n", + " if job_step.name == \"training\":\n", + " for task in job_step.tasks or []:\n", + " value = (task.status_details or {}).get(metric_key)\n", + " if value is not None:\n", + " return float(value)\n", + " return None\n", + "\n", + "\n", + "# ---------------------------------------------------------------------------\n", + "# Job polling with live dashboard\n", + "# ---------------------------------------------------------------------------\n", + "\n", + "def wait_for_job(\n", + " workspace: str,\n", + " job_name: str,\n", + " timeout: int = TIMEOUT_SECONDS,\n", + " poll_interval: int = 10,\n", + " val_loss_key: str = VAL_LOSS_KEY,\n", + " train_loss_key: str = TRAIN_LOSS_KEY,\n", + ") -> PlatformJobStatusResponse:\n", + " \"\"\"\n", + " Poll job status until completed, failed, cancelled, or timeout.\n", + " Displays a live dashboard with loss curves and GPU metrics.\n", + "\n", + " Args:\n", + " workspace: The workspace where the job is running.\n", + " job_name: The name of the job to monitor.\n", + " timeout: Maximum time to wait in seconds (default: 30 minutes).\n", + " poll_interval: Time between status checks in seconds (default: 10).\n", + "\n", + " Returns:\n", + " The final job status response.\n", + " \"\"\"\n", + " start_time = time.time()\n", + "\n", + " # Time-series accumulators required for plotting\n", + " elapsed_mins: list[float] = []\n", + " val_losses: list[float | None] = []\n", + " train_losses: list[float | None] = []\n", + " vram_history: list[list[float]] = []\n", + " util_history: list[list[float]] = []\n", + "\n", + " while True:\n", + " elapsed = time.time() - start_time\n", + " elapsed_min = elapsed / 60\n", + "\n", + " # Check for timeout\n", + " if elapsed > timeout:\n", + " error_message = f\"Timeout reached after {elapsed_min:.1f} minutes\"\n", + " print(f\"\\n{error_message}\")\n", + " print(\"Job did not complete within the timeout period.\")\n", + " raise Exception(error_message)\n", + "\n", + " status = client.jobs.get_status(name=job_name, workspace=workspace)\n", + "\n", + " # -- Extract training progress from nested steps structure --\n", + " step: int | None = None\n", + " max_steps: int | None = None\n", + " training_phase: str | None = None\n", + " val_loss: float | None = None\n", + " train_loss: float | None = None\n", + " current_step_name: str | None = None\n", + " current_step_phase: str | None = None\n", + "\n", + " for job_step in status.steps or []:\n", + " # Track the current active step name and phase for progress display\n", + " if job_step.tasks:\n", + " task = job_step.tasks[0]\n", + " td = task.status_details or {}\n", + " phase = cast(str, td.get(\"phase\", \"\"))\n", + " # Update current step if it's active or pending (not completed)\n", + " if job_step.status in (\"active\", \"pending\"):\n", + " current_step_name = job_step.name\n", + " current_step_phase = phase or \"started\"\n", + "\n", + " if job_step.name == \"training\":\n", + " for task in job_step.tasks or []:\n", + " td = task.status_details or {}\n", + " step = cast(int, td[\"step\"]) if \"step\" in td else None\n", + " max_steps = cast(int, td[\"max_steps\"]) if \"max_steps\" in td else None\n", + " training_phase = cast(str, td[\"phase\"]) if \"phase\" in td else None\n", + " raw_val_loss = td.get(val_loss_key)\n", + " val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n", + " raw_train_loss = td.get(train_loss_key)\n", + " train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n", + " break\n", + " break\n", + "\n", + " if val_loss is None:\n", + " raw_val_loss = (status.status_details or {}).get(val_loss_key)\n", + " val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n", + " if train_loss is None:\n", + " raw_train_loss = (status.status_details or {}).get(train_loss_key)\n", + " train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n", + "\n", + " # -- Collect GPU snapshot --\n", + " vram_pcts, util_pcts = _get_gpu_snapshot()\n", + "\n", + " # -- Append to accumulators used for the plots --\n", + " elapsed_mins.append(elapsed_min)\n", + " val_losses.append(val_loss)\n", + " train_losses.append(train_loss)\n", + " vram_history.append(vram_pcts)\n", + " util_history.append(util_pcts)\n", + "\n", + " # -- Build status strings --\n", + " status_str = f\"Status: {status.status}\"\n", + " if step is not None and max_steps is not None:\n", + " pct = step / max_steps * 100\n", + " step_str = f\"Step {step}/{max_steps} ({pct:.0f}%)\"\n", + " if training_phase:\n", + " step_str += f\" - {training_phase}\"\n", + " else:\n", + " if current_step_name and current_step_phase:\n", + " step_str = f\"{current_step_name} - {current_step_phase}\"\n", + " elif current_step_name:\n", + " step_str = f\"{current_step_name}\"\n", + " else:\n", + " step_str = \"Waiting for training to start...\"\n", + " elapsed_str = f\"Elapsed: {elapsed_min:.1f} min\"\n", + "\n", + " # -- Redraw dashboard --\n", + " clear_output(wait=True)\n", + " _draw_dashboard(\n", + " elapsed_mins, val_losses, train_losses,\n", + " vram_history, util_history,\n", + " job_name, status_str, step_str, elapsed_str,\n", + " )\n", + "\n", + " # -- Check terminal conditions --\n", + " if status.status.lower() == \"completed\":\n", + " # Redraw dashboard one final time with \"completed\" status\n", + " status_str = f\"Status: {status.status}\"\n", + " if step is not None and max_steps is not None:\n", + " step_str = f\"Step {max_steps}/{max_steps} (100%)\"\n", + " clear_output(wait=True)\n", + " _draw_dashboard(\n", + " elapsed_mins, val_losses, train_losses,\n", + " vram_history, util_history,\n", + " job_name, status_str, step_str, elapsed_str,\n", + " )\n", + " print(f\"\\nJob completed in {elapsed_min:.1f} minutes ({elapsed:.0f}s)\")\n", + " return status\n", + " elif status.status.lower() in (\"failed\", \"cancelled\", \"error\"):\n", + " print(f\"\\nJob finished with status: {status.status}\")\n", + " print(f\"Total time elapsed: {elapsed_min:.1f} minutes ({elapsed:.0f}s)\")\n", + "\n", + " # Print error details from the job level\n", + " if status.error_details:\n", + " error_msg = status.error_details.get(\"message\", \"\")\n", + " if error_msg:\n", + " print(f\"\\nError: {error_msg}\")\n", + "\n", + " # Find and print error details from the failed step/task\n", + " for job_step in status.steps or []:\n", + " if job_step.status == \"error\":\n", + " print(f\"\\nFailed step: {job_step.name}\")\n", + " if job_step.error_details:\n", + " step_error = job_step.error_details.get(\"message\", \"\")\n", + " if step_error:\n", + " print(f\"Step error: {step_error}\")\n", + " # Get error_stack from the failed task\n", + " for task in job_step.tasks or []:\n", + " if task.status == \"error\" and hasattr(task, \"error_stack\") and task.error_stack:\n", + " print(f\"\\nError stack trace:\\n{task.error_stack}\")\n", + " elif task.status == \"error\" and task.error_details:\n", + " task_error = task.error_details.get(\"message\", \"\")\n", + " if task_error:\n", + " print(f\"Task error: {task_error}\")\n", + " break\n", + "\n", + " raise Exception(f\"Job finished with status: {status.status}\")\n", + "\n", + " time.sleep(poll_interval)\n", + "\n", + "\n", + "# Wait for the job to complete\n", + "job_with_sequence_packing_status = wait_for_job(\n", + " workspace=\"default\",\n", + " job_name=job_with_sequence_packing.job.name,\n", + " timeout=TIMEOUT_SECONDS,\n", + ")\n", + "\n", + "packed_val_loss = get_training_metric(job_with_sequence_packing_status, VAL_LOSS_KEY)\n", + "if packed_val_loss is not None:\n", + " print(f\"Validation loss: {packed_val_loss:.2f}\")\n", + "else:\n", + " print(\"Validation loss: not reported in job status\")\n" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7. Create LoRA Job without Sequence Packing\n", + "Create a second Automodel LoRA job with `batch.sequence_packing=False` for comparison." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import uuid\n", + "from nemo_automodel_plugin.schema import AutomodelJobInput\n", + "\n", + "job_suffix = uuid.uuid4().hex[:4]\n", + "JOB_NAME = f\"no-packing-job-{job_suffix}\"\n", + "NO_PACK_OUTPUT_NAME = f\"no-packing-out-{job_suffix}\"\n", + "\n", + "spec = AutomodelJobInput(\n", + " model=f\"default/{base_model.name}\",\n", + " dataset={\"training\": f\"default/{DATASET_NAME}\"},\n", + " training={\n", + " \"training_type\": \"sft\",\n", + " \"finetuning_type\": \"lora\",\n", + " \"max_seq_length\": 4096,\n", + " },\n", + " schedule={\"epochs\": 1, \"val_check_interval\": 0.1},\n", + " batch={\n", + " \"global_batch_size\": 64,\n", + " \"micro_batch_size\": 1,\n", + " \"sequence_packing\": False,\n", + " },\n", + " optimizer={\"learning_rate\": 5e-5},\n", + " parallelism={\"num_gpus_per_node\": 1},\n", + " output={\"name\": NO_PACK_OUTPUT_NAME},\n", + ")\n", + "\n", + "job_without_sequence_packing = client.customization.automodel.jobs.create(\n", + " spec=spec, workspace=\"default\", name=JOB_NAME\n", + ")\n", + "\n", + "print(f\"Submitted job: {job_without_sequence_packing.job.name}\")\n", + "print(f\"Output adapter: {NO_PACK_OUTPUT_NAME}\")\n" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 8. Track Fine-Tuning Progress for Job without Sequence Packing" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "# Wait for the training step to complete\n", + "job_without_sequence_packing_status = wait_for_job(\n", + " workspace=\"default\",\n", + " job_name=job_without_sequence_packing.job.name,\n", + " timeout=TIMEOUT_SECONDS\n", + ")\n", + "\n", + "no_pack_val_loss = get_training_metric(job_without_sequence_packing_status, VAL_LOSS_KEY)\n", + "if no_pack_val_loss is not None:\n", + " print(f\"Validation loss: {no_pack_val_loss:.2f}\")\n", + "else:\n", + " print(\"Validation loss: not reported in job status\")" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 9. Compare Results\n", + "- Time to complete training should be significantly lower for the job that used sequence packing.\n", + "- The expected validation loss for both jobs should be similar.\n", + "- Sequence packed version should have a higher GPU utilization and higher GPU Memory Allocation." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "from nemo_platform.types.jobs import PlatformJobStep\n", + "import pandas as pd\n", + "\n", + "STEP_NAME = \"training\"\n", + "\n", + "def get_elapsed_time(step: PlatformJobStep) -> float:\n", + " \"\"\"Calculate elapsed time in seconds from step's created_at to updated_at.\"\"\"\n", + " if step.created_at is None or step.updated_at is None:\n", + " raise ValueError(\"Training step timestamps are unavailable\")\n", + " return (step.updated_at - step.created_at).total_seconds()\n", + "\n", + "step_with_sequence_packing = client.jobs.steps.retrieve(\n", + " name=STEP_NAME,\n", + " workspace=\"default\",\n", + " job=job_with_sequence_packing.job.name,\n", + ")\n", + "\n", + "step_without_sequence_packing = client.jobs.steps.retrieve(\n", + " name=STEP_NAME,\n", + " workspace=\"default\",\n", + " job=job_without_sequence_packing.job.name,\n", + ")\n", + "\n", + "time_to_complete_with_sequence_packing = get_elapsed_time(step_with_sequence_packing)\n", + "time_to_complete_without_sequence_packing = get_elapsed_time(step_without_sequence_packing)\n", + "\n", + "# Display results as a table\n", + "results_df = pd.DataFrame({\n", + " \"Seq Packing Enabled\": [True, False],\n", + " \"Val Loss\": [packed_val_loss, no_pack_val_loss],\n", + " \"Training Step Time, sec\": [\n", + " time_to_complete_with_sequence_packing,\n", + " time_to_complete_without_sequence_packing\n", + " ]\n", + "})\n", + "\n", + "results_df.style.format({\"Val Loss\": \"{:.2f}\", \"Training Step Time, sec\": \"{:.0f}\"}).hide(axis='index')" + ], + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Examples of Validation Loss\n", + "\n", + "The expected validation loss curves should match closely for both jobs.\n", + "![Validation loss comparison chart showing similar convergence patterns between sequence-packed and non-packed training runs over training steps](../_images/packed_vs_not_packed_val_loss.png)\n", + "\n", + "Sequence packed version should complete significantly faster.\n", + "![Runtime comparison chart demonstrating significantly reduced training time for sequence-packed job compared to non-packed baseline](../_images/runtime.png)\n", + "\n", + "#### GPU Utilization\n", + "Sequence packed version should have a higher GPU utilization.\n", + "![GPU utilization chart showing higher and more consistent GPU usage with sequence packing enabled throughout the training process](../_images/gpu_utilization.png)\n", + "\n", + "#### GPU Memory Allocation\n", + "Sequence packed version should have a higher GPU Memory Allocation.\n", + "![GPU memory allocation chart illustrating increased memory utilization efficiency with sequence packing enabled](../_images/gpu_memory.png)\n", + "\n", + "## Next Steps\n", + "\n", + "- [Monitor customization metrics](fine-tune-metrics) for training and validation loss.\n", + "- [Create a LoRA customization job](./lora-customization-job).\n", + "- [Create a Full SFT customization job](./sft-customization-job)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.14" } - }, - "outputs": [], - "source": [ - "if command -v uv >/dev/null 2>&1 && [ -n \"$VIRTUAL_ENV\" ]; then\n", - " uv pip install datasets pandas matplotlib nvidia-ml-py\n", - "else\n", - " pip install datasets pandas matplotlib nvidia-ml-py\n", - "fi" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Download rajpurkar/squad Dataset\n", - "\n", - "SQuAD (Stanford Question Answering Dataset) is a reading comprehension dataset consisting of questions posed on Wikipedia articles, where the answer is a segment of text from the corresponding passage." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import json\n", - "import os\n", - "from pathlib import Path\n", - "from datasets import load_dataset, Dataset, DatasetDict\n", - "\n", - "# Configuration\n", - "SEED = 1234\n", - "DATASET_NAME = \"sft-dataset\"\n", - "\n", - "# Convert SQuAD format to prompt/completion format and save to JSONL\n", - "def convert_squad_to_sft_format(example):\n", - " \"\"\"Convert SQuAD format to prompt/completion format for SFT training.\"\"\"\n", - " prompt = f\"Context: {example['context']} Question: {example['question']} Answer:\"\n", - " completion = example[\"answers\"][\"text\"][0] # Take the first answer\n", - " return {\"prompt\": prompt, \"completion\": completion}\n", - "\n", - "# Load the SQuAD dataset from Hugging Face\n", - "print(\"Loading dataset rajpurkar/squad\")\n", - "ds = load_dataset(\"rajpurkar/squad\")\n", - "if not isinstance(ds, DatasetDict):\n", - " raise ValueError(\"Dataset does not contain expected splits\")\n", - "\n", - "print(\"Loaded dataset\")\n", - "\n", - "# For the purpose of this tutorial, we'll use a subset of the dataset\n", - "# We use a reduced dataset size (3000 training/300 validation samples) to keep tutorial runtime manageable\n", - "# while still demonstrating the performance benefits of sequence packing. The larger the dataset,\n", - "# the better the model will perform but the longer the training will take.\n", - "training_size = 3000\n", - "validation_size = 300\n", - "DATASET_PATH = Path(DATASET_NAME).absolute()\n", - "\n", - "# Get training split and verify it's a Dataset (not IterableDataset)\n", - "train_dataset = ds[\"train\"]\n", - "validation_dataset = ds[\"validation\"]\n", - "assert isinstance(train_dataset, Dataset), \"Expected Dataset type\"\n", - "assert isinstance(validation_dataset, Dataset), \"Expected Dataset type\"\n", - "\n", - "# Select subsets and save to JSONL files\n", - "training_ds = train_dataset.select(range(training_size))\n", - "validation_ds = validation_dataset.select(range(validation_size))\n", - "\n", - "# Transform to SFT format (prompt/completion)\n", - "training_ds = training_ds.map(convert_squad_to_sft_format, remove_columns=training_ds.column_names)\n", - "validation_ds = validation_ds.map(convert_squad_to_sft_format, remove_columns=validation_ds.column_names)\n", - "\n", - "# Create directory if it doesn't exist\n", - "# Note: This will create a local 'sft-dataset/' directory with training.jsonl and validation.jsonl files\n", - "os.makedirs(DATASET_PATH, exist_ok=True)\n", - "\n", - "# Save subsets to JSONL files\n", - "training_ds.to_json(f\"{DATASET_PATH}/training.jsonl\")\n", - "validation_ds.to_json(f\"{DATASET_PATH}/validation.jsonl\")\n", - "\n", - "print(f\"Saved training.jsonl with {len(training_ds)} rows\")\n", - "print(f\"Saved validation.jsonl with {len(validation_ds)} rows\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Create fileset to store SFT training data\n", - "\n", - "try:\n", - " client.files.filesets.create(\n", - " workspace=\"default\",\n", - " name=DATASET_NAME,\n", - " description=\"SFT training data\"\n", - " )\n", - " print(f\"Created fileset: {DATASET_NAME}\")\n", - "except ConflictError:\n", - " print(f\"Fileset '{DATASET_NAME}' already exists, continuing...\")\n", - "\n", - "# Upload training data files individually to ensure correct structure\n", - "client.files.upload(\n", - " local_path=f\"{DATASET_PATH}/\", # Trailing slash uploads directory contents to fileset root\n", - " remote_path=\"\",\n", - " fileset=DATASET_NAME,\n", - " workspace=\"default\"\n", - ")\n", - "\n", - "# Validate training data is uploaded correctly\n", - "print(\"Training data:\")\n", - "print(json.dumps([f.model_dump() for f in client.files.list(fileset=DATASET_NAME, workspace=\"default\").data], indent=2))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 3. Secrets Setup\n", - "\n", - "If you plan to use NGC or HuggingFace models, you will need to configure authentication:\n", - "\n", - "- **NGC models** (`ngc://` URIs): Requires NGC API key\n", - "- **HuggingFace models** (`hf://` URIs): Requires HF token for gated/private models\n", - "\n", - "\n", - "Configure these as secrets in your platform. Refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md) for detailed instructions.\n", - "\n", - "Get your credentials to access base models:\n", - "- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n", - "- [HuggingFace Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n", - "\n", - "\n", - "---\n", - "\n", - "#### Quick Setup Example\n", - "\n", - "This tutorial uses the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from HuggingFace. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access.\n", - "\n", - "**HuggingFace Authentication:**\n", - "- For gated models (Llama, Gemma), you must provide a HuggingFace token via the `token_secret` parameter\n", - "- Get your token from [HuggingFace Settings](https://huggingface.co/settings/tokens) (requires Read access)\n", - "- Accept the model's terms on the HuggingFace model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main)\n", - "- For public models, you can omit the `token_secret` parameter when creating a fileset for the model in the next step." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\n", - "HF_TOKEN = os.getenv(\"HF_TOKEN\")\n", - "NGC_API_KEY = os.getenv(\"NGC_API_KEY\")\n", - "\n", - "\n", - "def create_or_get_secret(name: str, value: str | None, label: str):\n", - " if not value:\n", - " raise ValueError(f\"{label} environment variable is not set. Set it and try again.\")\n", - " try:\n", - " secret = client.secrets.create(\n", - " name=name,\n", - " workspace=\"default\",\n", - " value=value,\n", - " )\n", - " print(f\"Created secret: {name}\")\n", - " return secret\n", - " except ConflictError:\n", - " print(f\"Secret '{name}' already exists, continuing...\")\n", - " return client.secrets.retrieve(name=name, workspace=\"default\")\n", - "\n", - "\n", - "# Create HuggingFace token secret\n", - "hf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\n", - "print(\"HF_TOKEN secret:\")\n", - "print(hf_secret.model_dump_json(indent=2))\n", - "\n", - "# Create NGC API key secret\n", - "# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n", - "# ngc_api_key = create_or_get_secret(\"ngc-api-key\", NGC_API_KEY, \"NGC_API_KEY\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 4. Create Base Model FileSet\n", - "\n", - "Create a fileset pointing to the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model on HuggingFace. This step creates a pointer to the model on Hugging Face and does not download it. The model is downloaded at job creation time.\n", - "\n", - "Note: for public models, you can omit the `token_secret` parameter when creating a model fileset." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import time\n", - "from nemo_platform.types.files import HuggingfaceStorageConfigParam\n", - "\n", - "HF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\n", - "MODEL_NAME = \"llama-3-2-1b-base\"\n", - "\n", - "# Ensure you have a HuggingFace token secret created\n", - "# Create a fileset pointing to the desired HuggingFace model\n", - "try:\n", - " base_model_fs = client.files.filesets.create(\n", - " workspace=\"default\",\n", - " name=MODEL_NAME,\n", - " description=\"Llama 3.2 1B base model from HuggingFace\",\n", - " storage=HuggingfaceStorageConfigParam(\n", - " type=\"huggingface\",\n", - " # repo_id is the full model name from Hugging Face\n", - " repo_id=HF_REPO_ID,\n", - " repo_type=\"model\",\n", - " # we use the secret created in the previous step\n", - " token_secret=hf_secret.name\n", - " )\n", - " )\n", - " print(f\"Created base model fileset: {MODEL_NAME}\")\n", - "except ConflictError:\n", - " print(f\"Base model fileset already exists. Skipping creation.\")\n", - " base_model_fs = client.files.filesets.retrieve(\n", - " workspace=\"default\",\n", - " name=MODEL_NAME,\n", - " )\n", - "\n", - "# Create the Model Entity representation.\n", - "try:\n", - " base_model = client.models.create(\n", - " workspace=\"default\",\n", - " name=MODEL_NAME,\n", - " fileset=f\"default/{MODEL_NAME}\",\n", - " )\n", - " print(f\"Created Model Entity: {MODEL_NAME}\")\n", - "except ConflictError:\n", - " print(f\"Base model already exists. Updating fileset if different.\")\n", - " base_model = client.models.update(\n", - " workspace=\"default\",\n", - " name=MODEL_NAME,\n", - " fileset=f\"default/{MODEL_NAME}\",\n", - " )\n", - "\n", - "print(f\"\\nBase model fileset: fileset://default/{base_model.name}\")\n", - "print(\"Base model fileset files list:\")\n", - "print(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace=\"default\").data], indent=2))\n", - "\n", - "# Wait for ModelSpec to be populated from the checkpoint\n", - "print(\"\\nWaiting for ModelSpec to be populated...\")\n", - "SPEC_TIMEOUT_SECONDS = 120\n", - "spec_start = time.time()\n", - "while not base_model.spec:\n", - " if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n", - " raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds\")\n", - " time.sleep(2)\n", - " base_model = client.models.retrieve(\n", - " workspace=\"default\",\n", - " name=MODEL_NAME,\n", - " )\n", - "\n", - "print(f\"ModelSpec populated: {base_model.spec}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 5. Create LoRA Job with Sequence Packing\n", - "Create a LoRA customization job with **sequence packing** enabled via `AutomodelJobInput` (`batch.sequence_packing=True`)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import uuid\n", - "from nemo_automodel_plugin.schema import AutomodelJobInput\n", - "\n", - "SEQUENCE_PACKING_ENABLED = True\n", - "\n", - "job_suffix = uuid.uuid4().hex[:4]\n", - "JOB_NAME = f\"packing-job-{job_suffix}\"\n", - "PACK_OUTPUT_NAME = f\"packing-out-{job_suffix}\"\n", - "\n", - "spec = AutomodelJobInput(\n", - " model=f\"default/{base_model.name}\",\n", - " dataset={\"training\": f\"default/{DATASET_NAME}\"},\n", - " training={\n", - " \"training_type\": \"sft\",\n", - " \"finetuning_type\": \"lora\",\n", - " \"max_seq_length\": 4096,\n", - " },\n", - " schedule={\"epochs\": 1, \"val_check_interval\": 0.1},\n", - " batch={\n", - " \"global_batch_size\": 64,\n", - " \"micro_batch_size\": 1,\n", - " \"sequence_packing\": SEQUENCE_PACKING_ENABLED,\n", - " },\n", - " optimizer={\"learning_rate\": 5e-5},\n", - " parallelism={\"num_gpus_per_node\": 1},\n", - " output={\"name\": PACK_OUTPUT_NAME},\n", - ")\n", - "\n", - "job_with_sequence_packing = client.customization.automodel.jobs.create(\n", - " spec=spec, workspace=\"default\", name=JOB_NAME\n", - ")\n", - "\n", - "print(f\"Submitted job: {job_with_sequence_packing.job.name}\")\n", - "print(f\"Output adapter: {PACK_OUTPUT_NAME}\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 6. Track Finetuning Progress\n", - "\n", - "A training job contains multiple steps: \n", - "- Model and dataset downloading\n", - "- Finetuning where LoRA adapter weights are trained\n", - "- Creating a fileset entry for the finetuned model\n", - "- Finetuned weights uploading\n", - "\n", - "The elapsed time printed below reflects progress of the entire job. We compare the time taken by the finetuning step for both jobs in the last section of this tutorial." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Define Helper Functions" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "\n", - "# Helpers to draw GPU VRAM Utilization and Validation Loss\n", - "import matplotlib.pyplot as plt\n", - "try:\n", - " import pynvml\n", - " _PYNVML_AVAILABLE = True\n", - "except ImportError:\n", - " _PYNVML_AVAILABLE = False\n", - " print(\"Note: Install nvidia-ml-py ('pip install nvidia-ml-py' or 'uv pip install nvidia-ml-py') to enable live GPU metrics.\")\n", - "\n", - "# ---------------------------------------------------------------------------\n", - "# GPU metrics collection (nvidia-ml-py; import name is pynvml)\n", - "# ---------------------------------------------------------------------------\n", - "\n", - "def _get_gpu_snapshot() -> tuple[list[float], list[float]]:\n", - " \"\"\"Return (vram_usage_pcts, compute_util_pcts) for each GPU.\"\"\"\n", - " if not _PYNVML_AVAILABLE:\n", - " return [], []\n", - " pynvml.nvmlInit()\n", - " try:\n", - " vram, util = [], []\n", - " for i in range(pynvml.nvmlDeviceGetCount()):\n", - " h = pynvml.nvmlDeviceGetHandleByIndex(i)\n", - " mem = pynvml.nvmlDeviceGetMemoryInfo(h)\n", - " rates = pynvml.nvmlDeviceGetUtilizationRates(h)\n", - " vram.append(int(mem.used) / int(mem.total) * 100)\n", - " util.append(float(rates.gpu))\n", - " return vram, util\n", - " finally:\n", - " pynvml.nvmlShutdown()\n", - "\n", - "\n", - "# ---------------------------------------------------------------------------\n", - "# Dashboard drawing helpers\n", - "# ---------------------------------------------------------------------------\n", - "\n", - "_PALETTE = {\n", - " \"val_loss\": \"#E74C3C\",\n", - " \"train_loss\": \"#F39C12\",\n", - " \"vram\": [\"#3498DB\", \"#9B59B6\", \"#1ABC9C\", \"#E67E22\"],\n", - " \"util\": [\"#2ECC71\", \"#E74C3C\", \"#3498DB\", \"#F1C40F\"],\n", - " \"grid\": \"#ECECEC\",\n", - " \"title\": \"#2C3E50\",\n", - " \"subtitle\": \"#7F8C8D\",\n", - " \"spine\": \"#CCCCCC\",\n", - " \"tick\": \"#666666\",\n", - "}\n", - "\n", - "\n", - "def _style_axis(ax):\n", - " \"\"\"Apply shared cosmetic styling to a subplot axis.\"\"\"\n", - " ax.set_facecolor(\"white\")\n", - " ax.grid(True, alpha=0.4, color=_PALETTE[\"grid\"], linewidth=0.8)\n", - " for spine in (\"top\", \"right\"):\n", - " ax.spines[spine].set_visible(False)\n", - " ax.spines[\"left\"].set_color(_PALETTE[\"spine\"])\n", - " ax.spines[\"bottom\"].set_color(_PALETTE[\"spine\"])\n", - " ax.tick_params(colors=_PALETTE[\"tick\"], labelsize=9)\n", - "\n", - "\n", - "def _plot_line(ax, xs, ys, color, label, fill=True):\n", - " \"\"\"Plot a time series, gracefully skipping None values.\"\"\"\n", - " pts = [(x, y) for x, y in zip(xs, ys) if y is not None]\n", - " if not pts:\n", - " return\n", - " px, py = zip(*pts)\n", - " ax.plot(\n", - " px, py, color=color, linewidth=2.2,\n", - " marker=\"o\", markersize=4,\n", - " markerfacecolor=\"white\", markeredgewidth=1.8, markeredgecolor=color,\n", - " label=label, zorder=3,\n", - " )\n", - " if fill:\n", - " ax.fill_between(px, py, alpha=0.08, color=color)\n", - "\n", - "\n", - "def _plot_gpu_panel(ax, xs, history, colors, fallback_label):\n", - " \"\"\"Plot per-GPU time series with area fill.\"\"\"\n", - " if not history or not history[0]:\n", - " ax.text(\n", - " 0.5, 0.5, \"No GPU data\", transform=ax.transAxes,\n", - " ha=\"center\", va=\"center\", fontsize=11, color=\"#AAAAAA\",\n", - " )\n", - " return\n", - " n_gpus = max(len(snap) for snap in history)\n", - " for g in range(n_gpus):\n", - " vals = [snap[g] if g < len(snap) else 0 for snap in history]\n", - " c = colors[g % len(colors)]\n", - " label = f\"GPU {g}\" if n_gpus > 1 else fallback_label\n", - " ax.plot(xs[: len(vals)], vals, color=c, linewidth=2, label=label)\n", - " ax.fill_between(xs[: len(vals)], vals, alpha=0.08, color=c)\n", - " if n_gpus > 1:\n", - " ax.legend(fontsize=9, framealpha=0.9, edgecolor=\"#DDD\")\n", - "\n", - "\n", - "def _draw_dashboard(\n", - " elapsed_mins, val_losses, train_losses,\n", - " vram_history, util_history,\n", - " job_name, status_str, step_str, elapsed_str,\n", - "):\n", - " \"\"\"Render a live 1x3 training dashboard.\"\"\"\n", - " fig, axes = plt.subplots(1, 3, figsize=(20, 5.5))\n", - " fig.patch.set_facecolor(\"#FAFBFC\")\n", - "\n", - " fig.suptitle(\n", - " job_name, fontsize=15, fontweight=\"bold\",\n", - " color=_PALETTE[\"title\"], y=1.10,\n", - " )\n", - " fig.text(\n", - " 0.5, 1.01,\n", - " f\"{status_str} | {step_str} | {elapsed_str}\",\n", - " ha=\"center\", fontsize=13, color=_PALETTE[\"subtitle\"],\n", - " )\n", - "\n", - " for ax in axes:\n", - " _style_axis(ax)\n", - "\n", - " # -- Panel 1: Loss curves --\n", - " _plot_line(axes[0], elapsed_mins, val_losses, _PALETTE[\"val_loss\"], \"Val Loss\", fill=True)\n", - " _plot_line(axes[0], elapsed_mins, train_losses, _PALETTE[\"train_loss\"], \"Train Loss\", fill=False)\n", - " axes[0].set_title(\"Train/Validation Loss\", fontsize=13, fontweight=\"bold\", color=_PALETTE[\"title\"], pad=12)\n", - " axes[0].set_xlabel(\"Time (min)\", fontsize=10, color=\"#666\")\n", - " axes[0].set_ylabel(\"Loss\", fontsize=10, color=\"#666\")\n", - " if any(v is not None for v in val_losses + train_losses):\n", - " axes[0].legend(fontsize=9, framealpha=0.9, edgecolor=\"#DDD\")\n", - "\n", - " # -- Panel 2: GPU VRAM usage --\n", - " _plot_gpu_panel(axes[1], elapsed_mins, vram_history, _PALETTE[\"vram\"], \"VRAM\")\n", - " axes[1].set_title(\"GPU VRAM Usage\", fontsize=13, fontweight=\"bold\", color=_PALETTE[\"title\"], pad=12)\n", - " axes[1].set_xlabel(\"Time (min)\", fontsize=10, color=\"#666\")\n", - " axes[1].set_ylabel(\"Usage (%)\", fontsize=10, color=\"#666\")\n", - " axes[1].set_ylim(-2, 105)\n", - "\n", - " # -- Panel 3: GPU utilization --\n", - " _plot_gpu_panel(axes[2], elapsed_mins, util_history, _PALETTE[\"util\"], \"Utilization\")\n", - " axes[2].set_title(\"GPU Utilization\", fontsize=13, fontweight=\"bold\", color=_PALETTE[\"title\"], pad=12)\n", - " axes[2].set_xlabel(\"Time (min)\", fontsize=10, color=\"#666\")\n", - " axes[2].set_ylabel(\"Utilization (%)\", fontsize=10, color=\"#666\")\n", - " axes[2].set_ylim(-2, 105)\n", - "\n", - " plt.tight_layout(rect=[0, 0, 1, 0.98])\n", - " plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Monitor the Job Until Completion\n", - "\n", - "The cell below polls the job status every 10 seconds and renders a live dashboard with validation loss, GPU VRAM usage, and GPU utilization charts. The charts appear empty at first while the model and dataset download; training metrics and GPU activity populate after the finetuning step begins.\n", - "\n", - "> **Note:** This is additional code. You can also use the Weights & Biases or MLflow integrations." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import time\n", - "from typing import cast\n", - "from IPython.display import clear_output\n", - "from nemo_platform.types.shared import PlatformJobStatusResponse\n", - "\n", - "# Timeout set to 30 minutes to accommodate typical LoRA training duration for this dataset size.\n", - "# Actual training time will vary based on hardware, model size, and dataset complexity.\n", - "TIMEOUT_SECONDS = 30 * 60 # 30 minutes\n", - "VAL_LOSS_KEY = \"val_loss\"\n", - "TRAIN_LOSS_KEY = \"loss\"\n", - "\n", - "# ---------------------------------------------------------------------------\n", - "# Job polling with live dashboard\n", - "# ---------------------------------------------------------------------------\n", - "\n", - "def wait_for_job(\n", - " workspace: str,\n", - " job_name: str,\n", - " timeout: int = TIMEOUT_SECONDS,\n", - " poll_interval: int = 10,\n", - " val_loss_key: str = VAL_LOSS_KEY,\n", - " train_loss_key: str = TRAIN_LOSS_KEY,\n", - ") -> PlatformJobStatusResponse:\n", - " \"\"\"\n", - " Poll job status until completed, failed, cancelled, or timeout.\n", - " Displays a live dashboard with loss curves and GPU metrics.\n", - "\n", - " Args:\n", - " workspace: The workspace where the job is running.\n", - " job_name: The name of the job to monitor.\n", - " timeout: Maximum time to wait in seconds (default: 30 minutes).\n", - " poll_interval: Time between status checks in seconds (default: 10).\n", - "\n", - " Returns:\n", - " The final job status response.\n", - " \"\"\"\n", - " start_time = time.time()\n", - "\n", - " # Time-series accumulators required for plotting\n", - " elapsed_mins: list[float] = []\n", - " val_losses: list[float | None] = []\n", - " train_losses: list[float | None] = []\n", - " vram_history: list[list[float]] = []\n", - " util_history: list[list[float]] = []\n", - "\n", - " while True:\n", - " elapsed = time.time() - start_time\n", - " elapsed_min = elapsed / 60\n", - "\n", - " # Check for timeout\n", - " if elapsed > timeout:\n", - " error_message = f\"Timeout reached after {elapsed_min:.1f} minutes\"\n", - " print(f\"\\n{error_message}\")\n", - " print(\"Job did not complete within the timeout period.\")\n", - " raise Exception(error_message)\n", - "\n", - " status = client.jobs.get_status(name=job_name, workspace=workspace)\n", - "\n", - " # -- Extract training progress from nested steps structure --\n", - " step: int | None = None\n", - " max_steps: int | None = None\n", - " training_phase: str | None = None\n", - " val_loss: float | None = None\n", - " train_loss: float | None = None\n", - " current_step_name: str | None = None\n", - " current_step_phase: str | None = None\n", - "\n", - " for job_step in status.steps or []:\n", - " # Track the current active step name and phase for progress display\n", - " if job_step.tasks:\n", - " task = job_step.tasks[0]\n", - " td = task.status_details or {}\n", - " phase = cast(str, td.get(\"phase\", \"\"))\n", - " # Update current step if it's active or pending (not completed)\n", - " if job_step.status in (\"active\", \"pending\"):\n", - " current_step_name = job_step.name\n", - " current_step_phase = phase or \"started\"\n", - "\n", - " if job_step.name == \"training\":\n", - " for task in job_step.tasks or []:\n", - " td = task.status_details or {}\n", - " step = cast(int, td[\"step\"]) if \"step\" in td else None\n", - " max_steps = cast(int, td[\"max_steps\"]) if \"max_steps\" in td else None\n", - " training_phase = cast(str, td[\"phase\"]) if \"phase\" in td else None\n", - " raw_val_loss = td.get(val_loss_key)\n", - " val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n", - " raw_train_loss = td.get(train_loss_key)\n", - " train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n", - " break\n", - " break\n", - "\n", - " if val_loss is None:\n", - " raw_val_loss = (status.status_details or {}).get(val_loss_key)\n", - " val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n", - " if train_loss is None:\n", - " raw_train_loss = (status.status_details or {}).get(train_loss_key)\n", - " train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n", - "\n", - " # -- Collect GPU snapshot --\n", - " vram_pcts, util_pcts = _get_gpu_snapshot()\n", - "\n", - " # -- Append to accumulators used for the plots --\n", - " elapsed_mins.append(elapsed_min)\n", - " val_losses.append(val_loss)\n", - " train_losses.append(train_loss)\n", - " vram_history.append(vram_pcts)\n", - " util_history.append(util_pcts)\n", - "\n", - " # -- Build status strings --\n", - " status_str = f\"Status: {status.status}\"\n", - " if step is not None and max_steps is not None:\n", - " pct = step / max_steps * 100\n", - " step_str = f\"Step {step}/{max_steps} ({pct:.0f}%)\"\n", - " if training_phase:\n", - " step_str += f\" - {training_phase}\"\n", - " else:\n", - " if current_step_name and current_step_phase:\n", - " step_str = f\"{current_step_name} - {current_step_phase}\"\n", - " elif current_step_name:\n", - " step_str = f\"{current_step_name}\"\n", - " else:\n", - " step_str = \"Waiting for training to start...\"\n", - " elapsed_str = f\"Elapsed: {elapsed_min:.1f} min\"\n", - "\n", - " # -- Redraw dashboard --\n", - " clear_output(wait=True)\n", - " _draw_dashboard(\n", - " elapsed_mins, val_losses, train_losses,\n", - " vram_history, util_history,\n", - " job_name, status_str, step_str, elapsed_str,\n", - " )\n", - "\n", - " # -- Check terminal conditions --\n", - " if status.status.lower() == \"completed\":\n", - " # Redraw dashboard one final time with \"completed\" status\n", - " status_str = f\"Status: {status.status}\"\n", - " if step is not None and max_steps is not None:\n", - " step_str = f\"Step {max_steps}/{max_steps} (100%)\"\n", - " clear_output(wait=True)\n", - " _draw_dashboard(\n", - " elapsed_mins, val_losses, train_losses,\n", - " vram_history, util_history,\n", - " job_name, status_str, step_str, elapsed_str,\n", - " )\n", - " print(f\"\\nJob completed in {elapsed_min:.1f} minutes ({elapsed:.0f}s)\")\n", - " return status\n", - " elif status.status.lower() in (\"failed\", \"cancelled\", \"error\"):\n", - " print(f\"\\nJob finished with status: {status.status}\")\n", - " print(f\"Total time elapsed: {elapsed_min:.1f} minutes ({elapsed:.0f}s)\")\n", - "\n", - " # Print error details from the job level\n", - " if status.error_details:\n", - " error_msg = status.error_details.get(\"message\", \"\")\n", - " if error_msg:\n", - " print(f\"\\nError: {error_msg}\")\n", - "\n", - " # Find and print error details from the failed step/task\n", - " for job_step in status.steps or []:\n", - " if job_step.status == \"error\":\n", - " print(f\"\\nFailed step: {job_step.name}\")\n", - " if job_step.error_details:\n", - " step_error = job_step.error_details.get(\"message\", \"\")\n", - " if step_error:\n", - " print(f\"Step error: {step_error}\")\n", - " # Get error_stack from the failed task\n", - " for task in job_step.tasks or []:\n", - " if task.status == \"error\" and hasattr(task, \"error_stack\") and task.error_stack:\n", - " print(f\"\\nError stack trace:\\n{task.error_stack}\")\n", - " elif task.status == \"error\" and task.error_details:\n", - " task_error = task.error_details.get(\"message\", \"\")\n", - " if task_error:\n", - " print(f\"Task error: {task_error}\")\n", - " break\n", - "\n", - " raise Exception(f\"Job finished with status: {status.status}\")\n", - "\n", - " time.sleep(poll_interval)\n", - "\n", - "\n", - "# Wait for the job to complete\n", - "job_with_sequence_packing_status = wait_for_job(\n", - " workspace=\"default\",\n", - " job_name=job_with_sequence_packing.job.name,\n", - " timeout=TIMEOUT_SECONDS,\n", - ")\n", - "\n", - "packed_val_loss = (job_with_sequence_packing_status.status_details or {}).get(\"val_loss\")\n", - "if packed_val_loss is not None:\n", - " print(f\"Validation loss: {float(packed_val_loss):.2f}\")\n", - "else:\n", - " print(\"Validation loss: not reported in job status\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 7. Create LoRA Job without Sequence Packing\n", - "Create a second Automodel LoRA job with `batch.sequence_packing=False` for comparison." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import uuid\n", - "from nemo_automodel_plugin.schema import AutomodelJobInput\n", - "\n", - "job_suffix = uuid.uuid4().hex[:4]\n", - "JOB_NAME = f\"no-packing-job-{job_suffix}\"\n", - "NO_PACK_OUTPUT_NAME = f\"no-packing-out-{job_suffix}\"\n", - "\n", - "spec = AutomodelJobInput(\n", - " model=f\"default/{base_model.name}\",\n", - " dataset={\"training\": f\"default/{DATASET_NAME}\"},\n", - " training={\n", - " \"training_type\": \"sft\",\n", - " \"finetuning_type\": \"lora\",\n", - " \"max_seq_length\": 4096,\n", - " },\n", - " schedule={\"epochs\": 1, \"val_check_interval\": 0.1},\n", - " batch={\n", - " \"global_batch_size\": 64,\n", - " \"micro_batch_size\": 1,\n", - " \"sequence_packing\": False,\n", - " },\n", - " optimizer={\"learning_rate\": 5e-5},\n", - " parallelism={\"num_gpus_per_node\": 1},\n", - " output={\"name\": NO_PACK_OUTPUT_NAME},\n", - ")\n", - "\n", - "job_without_sequence_packing = client.customization.automodel.jobs.create(\n", - " spec=spec, workspace=\"default\", name=JOB_NAME\n", - ")\n", - "\n", - "print(f\"Submitted job: {job_without_sequence_packing.job.name}\")\n", - "print(f\"Output adapter: {NO_PACK_OUTPUT_NAME}\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 8. Track Finetuning Progress for Job without Sequence Packing" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Wait for the training step to complete\n", - "job_without_sequence_packing_status = wait_for_job(\n", - " workspace=\"default\",\n", - " job_name=job_without_sequence_packing.job.name,\n", - " timeout=TIMEOUT_SECONDS\n", - ")\n", - "\n", - "no_pack_val_loss = (job_without_sequence_packing_status.status_details or {}).get(\"val_loss\")\n", - "if no_pack_val_loss is not None:\n", - " print(f\"Validation loss: {float(no_pack_val_loss):.2f}\")\n", - "else:\n", - " print(\"Validation loss: not reported in job status\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### 9. Compare Results\n", - "- Time to complete training should be significantly lower for the job that used sequence packing.\n", - "- The expected validation loss for both jobs should be similar.\n", - "- Sequence packed version should have a higher GPU utilization and higher GPU Memory Allocation." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from nemo_platform.types.jobs import PlatformJobStep\n", - "from datetime import datetime\n", - "import pandas as pd\n", - "\n", - "STEP_NAME = \"training\"\n", - "\n", - "def get_elapsed_time(step: PlatformJobStep) -> float:\n", - " \"\"\"Calculate elapsed time in seconds from step's created_at to updated_at.\"\"\"\n", - " created_at = datetime.fromisoformat(step.created_at.replace(\"Z\", \"+00:00\"))\n", - " updated_at = datetime.fromisoformat(step.updated_at.replace(\"Z\", \"+00:00\"))\n", - " return (updated_at - created_at).total_seconds()\n", - "\n", - "step_with_sequence_packing = client.jobs.steps.retrieve(\n", - " name=STEP_NAME,\n", - " workspace=\"default\",\n", - " job=job_with_sequence_packing.job.name,\n", - ")\n", - "\n", - "step_without_sequence_packing = client.jobs.steps.retrieve(\n", - " name=STEP_NAME,\n", - " workspace=\"default\",\n", - " job=job_without_sequence_packing.job.name,\n", - ")\n", - "\n", - "time_to_complete_with_sequence_packing = get_elapsed_time(step_with_sequence_packing)\n", - "time_to_complete_without_sequence_packing = get_elapsed_time(step_without_sequence_packing)\n", - "\n", - "# Display results as a table\n", - "results_df = pd.DataFrame({\n", - " \"Seq Packing Enabled\": [True, False],\n", - " \"Val Loss\": [\n", - " (job_with_sequence_packing_status.status_details or {}).get(\"val_loss\"),\n", - " (job_without_sequence_packing_status.status_details or {}).get(\"val_loss\"),\n", - " ],\n", - " \"Training Step Time, sec\": [\n", - " time_to_complete_with_sequence_packing,\n", - " time_to_complete_without_sequence_packing\n", - " ]\n", - "})\n", - "\n", - "results_df.style.format({\"Val Loss\": \"{:.2f}\", \"Training Step Time, sec\": \"{:.0f}\"}).hide(axis='index')" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "#### Examples of Validation Loss\n", - "\n", - "The expected validation loss curves should match closely for both jobs.\n", - "![Validation loss comparison chart showing similar convergence patterns between sequence-packed and non-packed training runs over training steps](../_images/packed_vs_not_packed_val_loss.png)\n", - "\n", - "Sequence packed version should complete significantly faster.\n", - "![Runtime comparison chart demonstrating significantly reduced training time for sequence-packed job compared to non-packed baseline](../_images/runtime.png)\n", - "\n", - "#### GPU Utilization\n", - "Sequence packed version should have a higher GPU utilization.\n", - "![GPU utilization chart showing higher and more consistent GPU usage with sequence packing enabled throughout the training process](../_images/gpu_utilization.png)\n", - "\n", - "#### GPU Memory Allocation\n", - "Sequence packed version should have a higher GPU Memory Allocation.\n", - "![GPU memory allocation chart illustrating increased memory utilization efficiency with sequence packing enabled](../_images/gpu_memory.png)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": "python3" }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.14" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} + "nbformat": 4, + "nbformat_minor": 2 +} \ No newline at end of file diff --git a/docs/customizer/tutorials/optimize-throughput.mdx b/docs/customizer/tutorials/optimize-throughput.mdx index fa87e0fea4..07c2144bd6 100644 --- a/docs/customizer/tutorials/optimize-throughput.mdx +++ b/docs/customizer/tutorials/optimize-throughput.mdx @@ -5,8 +5,6 @@ description: "" [Run in Google Colab](https://colab.research.google.com/github/NVIDIA-NeMo/nemo-platform/blob/main/docs/customizer/tutorials/optimize-throughput.ipynb) -# Optimize for Tokens/GPU Throughput - ## About Learn how to use the NeMo Platform Customizer to create a [LoRA](/documentation/customizer-reference/customization-concepts#nemo-ms-about-concepts-customization) (Low-Rank Adaptation) customization job optimized for higher tokens/GPU throughput and lower runtime. @@ -23,6 +21,7 @@ Before starting this tutorial, ensure you have: 1. **Completed the [Quickstart](/documentation/get-started)** to install and deploy NeMo Platform locally 2. **Installed the Python SDK** (PyPI wrapper: `pip install "nemo-platform[all]"`; source checkout: run `make bootstrap` from the repository root) +3. **At least one GPU with CUDA 13+** ## Quick Start @@ -159,29 +158,29 @@ print(json.dumps([f.model_dump() for f in client.files.list(fileset=DATASET_NAME ### 3. Secrets Setup -If you plan to use NGC or HuggingFace models, you will need to configure authentication: +If you plan to use NGC or Hugging Face models, you will need to configure authentication: - **NGC models** (`ngc://` URIs): Requires NGC API key -- **HuggingFace models** (`hf://` URIs): Requires HF token for gated/private models +- **Hugging Face models** (`hf://` URIs): Requires HF token for gated/private models Configure these as secrets in your platform. Refer to [Managing Secrets](/documentation/get-started/core-concepts/manage-secrets) for detailed instructions. Get your credentials to access base models: - [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key) -- [HuggingFace Token](https://huggingface.co/settings/tokens) (Create token with Read access) +- [Hugging Face Token](https://huggingface.co/settings/tokens) (Create token with Read access) --- #### Quick Setup Example -This tutorial uses the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from HuggingFace. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access. +This tutorial uses the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from Hugging Face. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access. -**HuggingFace Authentication:** -- For gated models (Llama, Gemma), you must provide a HuggingFace token via the `token_secret` parameter -- Get your token from [HuggingFace Settings](https://huggingface.co/settings/tokens) (requires Read access) -- Accept the model's terms on the HuggingFace model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) +**Hugging Face Authentication:** +- For gated models (Llama, Gemma), you must provide a Hugging Face token via the `token_secret` parameter +- Get your token from [Hugging Face Settings](https://huggingface.co/settings/tokens) (requires Read access) +- Accept the model's terms on the Hugging Face model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) - For public models, you can omit the `token_secret` parameter when creating a fileset for the model in the next step. ```python @@ -206,7 +205,7 @@ def create_or_get_secret(name: str, value: str | None, label: str): return client.secrets.retrieve(name=name, workspace="default") -# Create HuggingFace token secret +# Create Hugging Face token secret hf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN") print("HF_TOKEN secret:") print(hf_secret.model_dump_json(indent=2)) @@ -218,7 +217,7 @@ print(hf_secret.model_dump_json(indent=2)) ### 4. Create Base Model FileSet -Create a fileset pointing to the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model on HuggingFace. This step creates a pointer to the model on Hugging Face and does not download it. The model is downloaded at job creation time. +Create a fileset pointing to the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model on Hugging Face. This step creates a pointer to the model on Hugging Face and does not download it. The model is downloaded at job creation time. Note: for public models, you can omit the `token_secret` parameter when creating a model fileset. @@ -229,13 +228,13 @@ from nemo_platform.types.files import HuggingfaceStorageConfigParam HF_REPO_ID = "meta-llama/Llama-3.2-1B-Instruct" MODEL_NAME = "llama-3-2-1b-base" -# Ensure you have a HuggingFace token secret created -# Create a fileset pointing to the desired HuggingFace model +# Ensure you have a Hugging Face token secret created +# Create a fileset pointing to the desired Hugging Face model try: base_model_fs = client.files.filesets.create( workspace="default", name=MODEL_NAME, - description="Llama 3.2 1B base model from HuggingFace", + description="Llama 3.2 1B base model from Hugging Face", storage=HuggingfaceStorageConfigParam( type="huggingface", # repo_id is the full model name from Hugging Face @@ -330,15 +329,15 @@ print(f"Output adapter: {PACK_OUTPUT_NAME}") ``` -### 6. Track Finetuning Progress +### 6. Track Fine-Tuning Progress A training job contains multiple steps: - Model and dataset downloading -- Finetuning where LoRA adapter weights are trained -- Creating a fileset entry for the finetuned model -- Finetuned weights uploading +- Fine-tuning where LoRA adapter weights are trained +- Creating a fileset entry for the fine-tuned model +- Fine-tuned weights uploading -The elapsed time printed below reflects progress of the entire job. We compare the time taken by the finetuning step for both jobs in the last section of this tutorial. +The elapsed time printed below reflects progress of the entire job. We compare the time taken by the fine-tuning step for both jobs in the last section of this tutorial. #### Define Helper Functions @@ -489,7 +488,7 @@ def _draw_dashboard( #### Monitor the Job Until Completion -The cell below polls the job status every 10 seconds and renders a live dashboard with validation loss, GPU VRAM usage, and GPU utilization charts. The charts appear empty at first while the model and dataset download; training metrics and GPU activity populate after the finetuning step begins. +The cell below polls the job status every 10 seconds and renders a live dashboard with validation loss, GPU VRAM usage, and GPU utilization charts. The charts appear empty at first while the model and dataset download; training metrics and GPU activity populate after the fine-tuning step begins. > **Note:** This is additional code. You can also use the Weights & Biases or MLflow integrations. @@ -503,7 +502,22 @@ from nemo_platform.types.shared import PlatformJobStatusResponse # Actual training time will vary based on hardware, model size, and dataset complexity. TIMEOUT_SECONDS = 30 * 60 # 30 minutes VAL_LOSS_KEY = "val_loss" -TRAIN_LOSS_KEY = "loss" +TRAIN_LOSS_KEY = "train_loss" + + +def get_training_metric( + status: PlatformJobStatusResponse, + metric_key: str, +) -> float | None: + """Return a metric reported by a task in the training step.""" + for job_step in status.steps or []: + if job_step.name == "training": + for task in job_step.tasks or []: + value = (task.status_details or {}).get(metric_key) + if value is not None: + return float(value) + return None + # --------------------------------------------------------------------------- # Job polling with live dashboard @@ -680,9 +694,9 @@ job_with_sequence_packing_status = wait_for_job( timeout=TIMEOUT_SECONDS, ) -packed_val_loss = (job_with_sequence_packing_status.status_details or {}).get("val_loss") +packed_val_loss = get_training_metric(job_with_sequence_packing_status, VAL_LOSS_KEY) if packed_val_loss is not None: - print(f"Validation loss: {float(packed_val_loss):.2f}") + print(f"Validation loss: {packed_val_loss:.2f}") else: print("Validation loss: not reported in job status") @@ -727,7 +741,7 @@ print(f"Output adapter: {NO_PACK_OUTPUT_NAME}") ``` -### 8. Track Finetuning Progress for Job without Sequence Packing +### 8. Track Fine-Tuning Progress for Job without Sequence Packing ```python # Wait for the training step to complete @@ -737,9 +751,9 @@ job_without_sequence_packing_status = wait_for_job( timeout=TIMEOUT_SECONDS ) -no_pack_val_loss = (job_without_sequence_packing_status.status_details or {}).get("val_loss") +no_pack_val_loss = get_training_metric(job_without_sequence_packing_status, VAL_LOSS_KEY) if no_pack_val_loss is not None: - print(f"Validation loss: {float(no_pack_val_loss):.2f}") + print(f"Validation loss: {no_pack_val_loss:.2f}") else: print("Validation loss: not reported in job status") ``` @@ -751,16 +765,15 @@ else: ```python from nemo_platform.types.jobs import PlatformJobStep -from datetime import datetime import pandas as pd STEP_NAME = "training" def get_elapsed_time(step: PlatformJobStep) -> float: """Calculate elapsed time in seconds from step's created_at to updated_at.""" - created_at = datetime.fromisoformat(step.created_at.replace("Z", "+00:00")) - updated_at = datetime.fromisoformat(step.updated_at.replace("Z", "+00:00")) - return (updated_at - created_at).total_seconds() + if step.created_at is None or step.updated_at is None: + raise ValueError("Training step timestamps are unavailable") + return (step.updated_at - step.created_at).total_seconds() step_with_sequence_packing = client.jobs.steps.retrieve( name=STEP_NAME, @@ -780,10 +793,7 @@ time_to_complete_without_sequence_packing = get_elapsed_time(step_without_sequen # Display results as a table results_df = pd.DataFrame({ "Seq Packing Enabled": [True, False], - "Val Loss": [ - (job_with_sequence_packing_status.status_details or {}).get("val_loss"), - (job_without_sequence_packing_status.status_details or {}).get("val_loss"), - ], + "Val Loss": [packed_val_loss, no_pack_val_loss], "Training Step Time, sec": [ time_to_complete_with_sequence_packing, time_to_complete_without_sequence_packing @@ -808,3 +818,9 @@ Sequence packed version should have a higher GPU utilization. #### GPU Memory Allocation Sequence packed version should have a higher GPU Memory Allocation. ![GPU memory allocation chart illustrating increased memory utilization efficiency with sequence packing enabled](../_images/gpu_memory.png) + +## Next Steps + +- [Monitor customization metrics](/documentation/customizer-reference/tutorials/metrics) for training and validation loss. +- [Create a LoRA customization job](/documentation/customizer-reference/tutorials/lora-customization-job). +- [Create a Full SFT customization job](/documentation/customizer-reference/tutorials/sft-customization-job). diff --git a/docs/customizer/tutorials/sft-customization-job.ipynb b/docs/customizer/tutorials/sft-customization-job.ipynb index d59af1f9a4..91d3c25899 100644 --- a/docs/customizer/tutorials/sft-customization-job.ipynb +++ b/docs/customizer/tutorials/sft-customization-job.ipynb @@ -29,7 +29,7 @@ "- ✅ Can fundamentally change model behavior\n", "- ✅ Best for significant domain shifts or specialized tasks\n", "- ❌ Requires substantial GPU resources (4-8x more than LoRA)\n", - "- ❌ Produces full model weights (~140GB for Llama 70B)\n", + "- ❌ Produces a full BF16 checkpoint (~140 GB for Llama 70B); peak job disk usage can reach approximately 3× the downloaded base checkpoint size\n", "- ❌ Longer training time\n", "\n", "**LoRA** trains only ~1% of weights by adding thin matrices to existing weights:\n", @@ -58,7 +58,8 @@ "Before starting this tutorial, ensure you have:\n", "\n", "1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n", - "2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)" + "2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n", + "3. **At least one GPU with CUDA 13+**" ] }, { @@ -306,29 +307,29 @@ "source": [ "### 4. Secrets Setup\n", "\n", - "If you plan to use NGC or HuggingFace models, you will need to configure authentication:\n", + "If you plan to use NGC or Hugging Face models, you will need to configure authentication:\n", "\n", "- **NGC models** (`ngc://` URIs): Requires NGC API key\n", - "- **HuggingFace models** (`hf://` URIs): Requires HF token for gated/private models\n", + "- **Hugging Face models** (`hf://` URIs): Requires HF token for gated/private models\n", "\n", "\n", "Configure these as secrets in your platform. Refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md) for detailed instructions.\n", "\n", "Get your credentials to access base models:\n", "- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n", - "- [HuggingFace Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n", + "- [Hugging Face Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n", "\n", "\n", "---\n", "\n", "#### Quick Setup Example\n", "\n", - "In this tutorial we are going to work with [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from HuggingFace. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access\n", + "In this tutorial we are going to work with the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from Hugging Face. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access.\n", "\n", - "**HuggingFace Authentication:**\n", - "- For gated models (Llama, Gemma), you must provide a HuggingFace token via the `token_secret` parameter\n", - "- Get your token from [HuggingFace Settings](https://huggingface.co/settings/tokens) (requires Read access)\n", - "- Accept the model's terms on the HuggingFace model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main)\n", + "**Hugging Face Authentication:**\n", + "- For gated models (Llama, Gemma), you must provide a Hugging Face token via the `token_secret` parameter\n", + "- Get your token from [Hugging Face Settings](https://huggingface.co/settings/tokens) (requires Read access)\n", + "- Accept the model's terms on the Hugging Face model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main)\n", "- For public models, you can omit the `token_secret` parameter when creating a fileset for model in the next step" ] }, @@ -336,14 +337,18 @@ "cell_type": "code", "metadata": {}, "source": [ - "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\n", + "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set.\n", + "# This tutorial's default model (meta-llama/Llama-3.2-1B-Instruct) is gated and requires HF_TOKEN.\n", "HF_TOKEN = os.getenv(\"HF_TOKEN\")\n", "NGC_API_KEY = os.getenv(\"NGC_API_KEY\")\n", + "if not HF_TOKEN:\n", + " raise RuntimeError(\n", + " \"Set HF_TOKEN before running this tutorial. \"\n", + " \"The default model meta-llama/Llama-3.2-1B-Instruct is gated.\"\n", + " )\n", "\n", "\n", - "def create_or_get_secret(name: str, value: str | None, label: str):\n", - " if not value:\n", - " raise ValueError(f\"{label} is not set\")\n", + "def create_or_get_secret(name: str, value: str, label: str):\n", " try:\n", " secret = client.secrets.create(\n", " name=name,\n", @@ -357,7 +362,7 @@ " return client.secrets.retrieve(name=name, workspace=\"default\")\n", "\n", "\n", - "# Create HuggingFace token secret\n", + "# Create Hugging Face token secret\n", "hf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\n", "print(\"HF_TOKEN secret:\")\n", "print(hf_secret.model_dump_json(indent=2))\n", @@ -375,9 +380,9 @@ "source": [ "### 5. Create Base Model FileSet and Model Entity\n", "\n", - "Create a fileset pointing to [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model in HuggingFace that we will train with SFT. Then create a Model Entity that references this fileset. Model downloading will take place at training time.\n", + "Create a fileset pointing to the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model in Hugging Face that we will train with SFT. Then create a Model Entity that references this fileset. Model downloading will take place at training time.\n", "\n", - "Note: for public models, you can omit the `token_secret` parameter when creating a model fileset." + "This tutorial's default model is gated, so the fileset includes `token_secret=hf_secret.name`. If you substitute a public model, you can omit `token_secret`." ] }, { @@ -386,26 +391,26 @@ "source": [ "import time\n", "\n", - "# Create a fileset pointing to the desired HuggingFace model\n", + "# Create a fileset pointing to the desired Hugging Face model\n", "from nemo_platform.types.files import HuggingfaceStorageConfigParam\n", "\n", "HF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\n", "MODEL_NAME = \"llama-3-2-1b-base\"\n", "\n", - "# Ensure you have a HuggingFace token secret created\n", + "# Ensure you have a Hugging Face token secret created\n", "try:\n", " base_model_fs = client.files.filesets.create(\n", " workspace=\"default\",\n", " name=MODEL_NAME,\n", - " description=\"Llama 3.2 1B base model from HuggingFace\",\n", + " description=\"Llama 3.2 1B base model from Hugging Face\",\n", " storage=HuggingfaceStorageConfigParam(\n", " type=\"huggingface\",\n", " # repo_id is the full model name from Hugging Face\n", " repo_id=HF_REPO_ID,\n", " repo_type=\"model\",\n", " # we use the secret created in the previous step\n", - " token_secret=hf_secret.name\n", - " )\n", + " token_secret=hf_secret.name,\n", + " ),\n", " )\n", " print(f\"Created base model fileset: {MODEL_NAME}\")\n", "except ConflictError:\n", @@ -457,7 +462,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 6. Create SFT Finetuning Job\n", + "### 6. Create SFT Fine-Tuning Job\n", "Create a customization job to fine-tune all model weights using the **Automodel** backend and `AutomodelJobInput`." ] }, @@ -689,8 +694,7 @@ ")\n", "```\n", "\n", - "**Single-Node Constraint:** Model deployments are limited to a single node. The maximum `gpu` value depends on the total GPUs available on a single node in your cluster. Multi-node deployments are not supported.\n", - "" + "**Single-Node Constraint:** Model deployments are limited to a single node. The maximum `gpu` value depends on the total GPUs available on a single node in your cluster. Multi-node deployments are not supported.\n" ] }, { @@ -827,7 +831,7 @@ "\n", "**Job fails during model download:**\n", "- Verify authentication secrets are configured (refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md))\n", - "- For gated HuggingFace models (Llama, Gemma), accept the license on the model page (for example, [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct))\n", + "- For gated Hugging Face models (Llama, Gemma), accept the license on the model page (for example, [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct))\n", "- Confirm the model fileset uses `token_secret=hf_secret.name` for gated models\n", "- Check `AutomodelJobInput` references use the `workspace/name` format: `model=f\"default/{MODEL_NAME}\"` and `dataset={\"training\": f\"default/{DATASET_NAME}\"}` (for example, `default/llama-3-2-1b-base`, `default/sft-dataset`)\n", "- Verify the model entity points at the fileset: `fileset=f\"default/{MODEL_NAME}\"`\n", @@ -885,4 +889,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/docs/customizer/tutorials/sft-customization-job.mdx b/docs/customizer/tutorials/sft-customization-job.mdx index 044abd8e01..b421b73359 100644 --- a/docs/customizer/tutorials/sft-customization-job.mdx +++ b/docs/customizer/tutorials/sft-customization-job.mdx @@ -5,8 +5,6 @@ description: "" [Run in Google Colab](https://colab.research.google.com/github/NVIDIA-NeMo/nemo-platform/blob/main/docs/customizer/tutorials/sft-customization-job.ipynb) -# Full SFT Customization - Learn how to fine-tune all model weights using supervised fine-tuning (SFT) to customize LLM behavior for your specific tasks. ## About @@ -27,7 +25,7 @@ Supervised Fine-Tuning (SFT) customizes model behavior, injects new knowledge, a - ✅ Can fundamentally change model behavior - ✅ Best for significant domain shifts or specialized tasks - ❌ Requires substantial GPU resources (4-8x more than LoRA) -- ❌ Produces full model weights (~140GB for Llama 70B) +- ❌ Produces a full BF16 checkpoint (~140 GB for Llama 70B); peak job disk usage can reach approximately 3× the downloaded base checkpoint size - ❌ Longer training time **LoRA** trains only ~1% of weights by adding thin matrices to existing weights: @@ -52,6 +50,7 @@ Before starting this tutorial, ensure you have: 1. **Completed the [Quickstart](/documentation/get-started)** to install and deploy NeMo Platform locally 2. **Installed the Python SDK** (PyPI wrapper: `pip install "nemo-platform[all]"`; source checkout: run `make bootstrap` from the repository root) +3. **At least one GPU with CUDA 13+** ## Quick Start @@ -224,40 +223,44 @@ print(json.dumps([f.model_dump() for f in client.files.list(fileset=DATASET_NAME ### 4. Secrets Setup -If you plan to use NGC or HuggingFace models, you will need to configure authentication: +If you plan to use NGC or Hugging Face models, you will need to configure authentication: - **NGC models** (`ngc://` URIs): Requires NGC API key -- **HuggingFace models** (`hf://` URIs): Requires HF token for gated/private models +- **Hugging Face models** (`hf://` URIs): Requires HF token for gated/private models Configure these as secrets in your platform. Refer to [Managing Secrets](/documentation/get-started/core-concepts/manage-secrets) for detailed instructions. Get your credentials to access base models: - [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key) -- [HuggingFace Token](https://huggingface.co/settings/tokens) (Create token with Read access) +- [Hugging Face Token](https://huggingface.co/settings/tokens) (Create token with Read access) --- #### Quick Setup Example -In this tutorial we are going to work with [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from HuggingFace. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access. +In this tutorial we are going to work with the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from Hugging Face. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access. -**HuggingFace Authentication:** -- For gated models (Llama, Gemma), you must provide a HuggingFace token via the `token_secret` parameter -- Get your token from [HuggingFace Settings](https://huggingface.co/settings/tokens) (requires Read access) -- Accept the model's terms on the HuggingFace model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) +**Hugging Face Authentication:** +- For gated models (Llama, Gemma), you must provide a Hugging Face token via the `token_secret` parameter +- Get your token from [Hugging Face Settings](https://huggingface.co/settings/tokens) (requires Read access) +- Accept the model's terms on the Hugging Face model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) - For public models, you can omit the `token_secret` parameter when creating a fileset for model in the next step ```python -# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set +# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set. +# This tutorial's default model (meta-llama/Llama-3.2-1B-Instruct) is gated and requires HF_TOKEN. HF_TOKEN = os.getenv("HF_TOKEN") NGC_API_KEY = os.getenv("NGC_API_KEY") +if not HF_TOKEN: + raise RuntimeError( + "Set HF_TOKEN before running this tutorial. " + "The default model meta-llama/Llama-3.2-1B-Instruct is gated." + ) -def create_or_get_secret(name: str, value: str | None, label: str): - if not value: - raise ValueError(f"{label} is not set") +def create_or_get_secret(name: str, value: str, label: str): try: secret = client.secrets.create( name=name, @@ -271,7 +274,7 @@ def create_or_get_secret(name: str, value: str | None, label: str): return client.secrets.retrieve(name=name, workspace="default") -# Create HuggingFace token secret +# Create Hugging Face token secret hf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN") print("HF_TOKEN secret:") print(hf_secret.model_dump_json(indent=2)) @@ -283,33 +286,33 @@ print(hf_secret.model_dump_json(indent=2)) ### 5. Create Base Model FileSet and Model Entity -Create a fileset pointing to [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model in HuggingFace that we will train with SFT. Then create a Model Entity that references this fileset. Model downloading will take place at training time. +Create a fileset pointing to the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model in Hugging Face that we will train with SFT. Then create a Model Entity that references this fileset. Model downloading will take place at training time. -Note: for public models, you can omit the `token_secret` parameter when creating a model fileset. +This tutorial's default model is gated, so the fileset includes `token_secret=hf_secret.name`. If you substitute a public model, you can omit `token_secret`. ```python import time -# Create a fileset pointing to the desired HuggingFace model +# Create a fileset pointing to the desired Hugging Face model from nemo_platform.types.files import HuggingfaceStorageConfigParam HF_REPO_ID = "meta-llama/Llama-3.2-1B-Instruct" MODEL_NAME = "llama-3-2-1b-base" -# Ensure you have a HuggingFace token secret created +# Ensure you have a Hugging Face token secret created try: base_model_fs = client.files.filesets.create( workspace="default", name=MODEL_NAME, - description="Llama 3.2 1B base model from HuggingFace", + description="Llama 3.2 1B base model from Hugging Face", storage=HuggingfaceStorageConfigParam( type="huggingface", # repo_id is the full model name from Hugging Face repo_id=HF_REPO_ID, repo_type="model", # we use the secret created in the previous step - token_secret=hf_secret.name - ) + token_secret=hf_secret.name, + ), ) print(f"Created base model fileset: {MODEL_NAME}") except ConflictError: @@ -355,7 +358,7 @@ while not base_model.spec: print(f"ModelSpec populated: {base_model.spec}") ``` -### 6. Create SFT Finetuning Job +### 6. Create SFT Fine-Tuning Job Create a customization job to fine-tune all model weights using the **Automodel** backend and `AutomodelJobInput`. **GPU Requirements:** @@ -655,7 +658,7 @@ For detailed information on all available hyperparameters, recommended values, a **Job fails during model download:** - Verify authentication secrets are configured (refer to [Managing Secrets](/documentation/get-started/core-concepts/manage-secrets)) -- For gated HuggingFace models (Llama, Gemma), accept the license on the model page (for example, [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)) +- For gated Hugging Face models (Llama, Gemma), accept the license on the model page (for example, [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)) - Confirm the model fileset uses `token_secret=hf_secret.name` for gated models - Check `AutomodelJobInput` references use the `workspace/name` format: `model=f"default/{MODEL_NAME}"` and `dataset={"training": f"default/{DATASET_NAME}"}` (for example, `default/llama-3-2-1b-base`, `default/sft-dataset`) - Verify the model entity points at the fileset: `fileset=f"default/{MODEL_NAME}"` diff --git a/docs/customizer/tutorials/understand-configurations-and-models.mdx b/docs/customizer/tutorials/understand-configurations-and-models.mdx index e9b9aa725d..bffd2ce61c 100644 --- a/docs/customizer/tutorials/understand-configurations-and-models.mdx +++ b/docs/customizer/tutorials/understand-configurations-and-models.mdx @@ -6,7 +6,7 @@ description: "" Learn the fundamentals of how NeMo Customizer works to make informed decisions about your fine-tuning projects. This tutorial covers how models are organized, how adapters attach to base models, training types and GPU requirements, and how to choose the right approach for your use case. -Understanding these basics will help you navigate the fine-tuning process more effectively and avoid common issues. If you're ready to start fine-tuning immediately, you can jump to [SFT Customization Job](sft-customization-job.ipynb) after completing this tutorial. +Understanding these basics will help you navigate the fine-tuning process more effectively and avoid common issues. If you're ready to start fine-tuning immediately, you can jump to [SFT Customization Job](/documentation/customizer-reference/tutorials/sft-customization-job) after completing this tutorial. @@ -42,7 +42,7 @@ An **Adapter** is a set of parameter-efficient fine-tuning weights (like LoRA) t - Are **nested within** the parent Model Entity - Are **enabled** for inference by default post training - Have their own FileSet for storing the adapter weights -- Track metadata like finetuning type, rank, and alpha values +- Track metadata like fine-tuning type, rank, and alpha values ### What is a FileSet? @@ -72,7 +72,7 @@ flowchart LR **1. Create a FileSet for your base model** -Upload your model checkpoint files (from HuggingFace, NGC, or local storage) to a FileSet: +Upload your model checkpoint files (from Hugging Face, NGC, or local storage) to a FileSet: ```python import os @@ -84,7 +84,7 @@ client = NeMoPlatform( workspace="default", ) -# Create a FileSet from HuggingFace +# Create a FileSet from Hugging Face fileset = client.files.filesets.create( workspace="default", name="llama-3-2-1b", @@ -93,7 +93,7 @@ fileset = client.files.filesets.create( type="huggingface", repo_id="meta-llama/Llama-3.2-1B-Instruct", repo_type="model", - token_secret="my-hf-token", # Secret containing HuggingFace token + token_secret="my-hf-token", # Secret containing Hugging Face token ), ) ``` @@ -240,12 +240,12 @@ model = client.models.adapters.create( ### Storage Requirements -Customization jobs consume disk space on the platform's shared persistent volume for model files, finetuning checkpoints, and the final output artifact. Required space depends on the training type: +Customization jobs consume disk space on the platform's shared persistent volume for model files, fine-tuning checkpoints, and the final output artifact. Required space depends on the training type: | Training Type | Approximate Disk Usage | Notes | |---------------|----------------------|-------| -| LoRA | ~1.5× base model size | Stores base model + small adapter weights | -| Full SFT | ~3× base model size | Stores base model + full checkpoint + output model | +| LoRA | ~1.5× downloaded base checkpoint size | Stores base model + small adapter weights | +| Full SFT | ~3× downloaded base checkpoint size | Stores base model + intermediate checkpoint + full output model | @@ -253,7 +253,7 @@ These estimates cover model weights only and do not include training dataset siz If the platform disk fills during a job, the job fails with an I/O error and the job service may return a ``500`` status when you retrieve logs. -Ensure your platform's shared persistent volume has at least **3× the base model size** +Ensure your platform's shared persistent volume has at least **3× the downloaded base checkpoint size** of free space before starting a full SFT job, or **1.5×** for LoRA jobs. @@ -261,15 +261,17 @@ For troubleshooting disk-related failures, see [customizer](/documentation/refer ### Parallelism Parameters Explained -Parallelism is configured via `training.parallelism`. These parameters control how training workloads are distributed across GPUs: +Parallelism is configured via the top-level Automodel `parallelism` block (for example, `parallelism={"num_gpus_per_node": 1}`). These parameters control how training workloads are distributed across GPUs: | Parameter | Description | Default | |-----------|-------------|---------| -| `tensor_parallel_size` | Number of GPUs to distribute each layer's parameters across | 1 | -| `pipeline_parallel_size` | Number of GPUs to distribute layers across sequentially | 1 | -| `context_parallel_size` | Number of GPUs to distribute sequence context across | 1 | -| `sequence_parallel` | Enable sequence parallelism to distribute activation memory along the sequence dimension | `false` | -| `expert_parallel_size` | Number of GPUs to distribute MoE experts across (MoE models only) | 1 | +| `parallelism.num_nodes` | Number of training nodes | `1` | +| `parallelism.num_gpus_per_node` | GPUs per node | `1` | +| `parallelism.tensor_parallel_size` | Number of GPUs to distribute each layer's parameters across | `1` | +| `parallelism.pipeline_parallel_size` | Number of GPUs to distribute layers across sequentially | `1` | +| `parallelism.context_parallel_size` | Number of GPUs to distribute sequence context across | `1` | +| `parallelism.sequence_parallel` | Enable sequence parallelism to distribute activation memory along the sequence dimension | `false` | +| `parallelism.expert_parallel_size` | Number of GPUs to distribute MoE experts across (MoE models only) | `null` | `data_parallel_size` is automatically derived as `total_gpus / (TP × PP × CP)` and is not set directly. @@ -277,16 +279,18 @@ Parallelism is configured via `training.parallelism`. These parameters control h **Recommended parallelism for Experts (MoE) Models**: -The `expert_parallel_size` parameter is used to parallelize a Mixture of Experts (MoE) model's experts across GPUs. For non-MoE models, this parameter is ignored. A model's model card will indicate if it is a Mixture of Experts model and specifies its number of experts. +The `parallelism.expert_parallel_size` parameter parallelizes a Mixture of Experts (MoE) model's experts across GPUs. For non-MoE models, leave it unset (`null`). A model's model card indicates whether it is a Mixture of Experts model and how many experts it has. -The number of experts in the model must be divisible by `expert_parallel_size`. For example, if a model has 8 experts, setting `expert_parallel_size=4` results in each GPU processing 2 experts. +When you set `expert_parallel_size`: +- The number of experts in the model must be divisible by `expert_parallel_size`. For example, if a model has 8 experts, `expert_parallel_size=4` gives each GPU 2 experts. +- `(data_parallel_size × context_parallel_size)` must be divisible by `expert_parallel_size`. +- When `expert_parallel_size > 1`, `tensor_parallel_size` must be `1`. -Also, the value of `expert_parallel_size` must evenly divide the derived `data_parallel_size`, which is automatically calculated as `data_parallel_size = total GPUs / (tensor_parallel_size × pipeline_parallel_size × context_parallel_size)`. - -For example, with 8 total GPUs, `tensor_parallel_size=2`, and `pipeline_parallel_size=1`: -- Derived `data_parallel_size = 8 / (2 × 1 × 1) = 4` -- Valid `expert_parallel_size` values: `1`, `2`, or `4` (must evenly divide 4) -- Invalid `expert_parallel_size` value: `3` (does not evenly divide 4) +For example, with 8 total GPUs, `tensor_parallel_size=1`, `pipeline_parallel_size=1`, and `context_parallel_size=1`: +- Derived `data_parallel_size = 8 / (1 × 1 × 1) = 8` +- `data_parallel_size × context_parallel_size = 8` +- Valid `expert_parallel_size` values: `1`, `2`, `4`, or `8` +- Invalid `expert_parallel_size` value: `3` (does not divide 8) ### Resource Allocation Rules @@ -359,7 +363,7 @@ flowchart TD | **Llama Models** | General-purpose language models excellent for instruction following, conversation, and text generation tasks | `llama-3.1-8b-instruct`, `llama-3.2-1b-instruct` | | **Llama Nemotron Models** | NVIDIA's specialized variants optimized for specific use cases with enhanced reasoning capabilities | Various Nano and Super variants | | **Phi Models** | Microsoft's efficient models designed for strong reasoning with optimized deployment characteristics | Phi model family configurations | -| **GPT-OSS Models** | Open-source GPT-based models supporting Full SFT customization workflows | Various GPT-OSS configurations | +| **GPT-OSS Models** | Open-weight reasoning models with tested Full SFT and LoRA configurations | `openai/gpt-oss-20b` | ### Specialized Models @@ -370,12 +374,12 @@ flowchart TD ### Importing Custom Models -You can import any HuggingFace-compatible model: +You can import a Hugging Face checkpoint into a FileSet and register it as a Model Entity: ```python from nemo_platform.types.files import HuggingfaceStorageConfigParam -# Create FileSet from HuggingFace +# Create FileSet from Hugging Face fileset = client.files.filesets.create( workspace="default", name="my-custom-model", @@ -393,7 +397,13 @@ model = client.models.create( ) ``` -For detailed guidance, see [Import HuggingFace Model](/documentation/customizer-reference/tutorials/import-hugging-face-models). +For detailed guidance, see [Import Hugging Face Model](/documentation/customizer-reference/tutorials/import-hugging-face-models). + + + +Importing a checkpoint does not guarantee that every training or deployment backend supports its architecture. In particular, Automodel LoRA does not support Conv1D-based architectures such as older GPT-2 variants. Confirm the model and fine-tuning regime in the [Tested Models](/documentation/customizer-reference/models/model-catalog) table, and review the import tutorial's known architecture limitations before submitting a job. + + --- @@ -408,19 +418,19 @@ Now that you understand how Model Entities and Adapters work, you're ready to pr Learn how to prepare your data for fine-tuning. - + Create a parameter-efficient LoRA adapter. - + Use full supervised fine-tuning for maximum performance. -Import and fine-tune private HuggingFace models. +Import and fine-tune private Hugging Face models. @@ -437,7 +447,7 @@ Import and fine-tune private HuggingFace models. ✅ **Full SFT training** creates a new Model Entity with full weights ✅ **Adapters are enabled by default** and automatically loaded by NIMs serving the base model ✅ **GPU requirements** vary significantly between LoRA and full fine-tuning -✅ **Custom HuggingFace models** can be imported via FileSet + Model Entity +✅ **Custom Hugging Face models** can be imported via FileSet + Model Entity ### Quick Reference Commands diff --git a/docs/fern/README.md b/docs/fern/README.md index bc339e89ee..115c1b2e3e 100644 --- a/docs/fern/README.md +++ b/docs/fern/README.md @@ -19,7 +19,7 @@ From the repo root (these wrap `cd docs/fern && npm run …`): ```bash make docs-deps # one-time: install docs/fern tooling (needed for MDX validation) make docs-login # one-time per machine: Fern CLI auth for the nvidia org -make docs-check # validate: fern check + MDX validation + gated-link check (what CI runs) +make docs-check # validate: fern check + MDX + NotebookViewer artifacts + gated links make docs # start local preview (prints a localhost URL) make docs-watch # start local preview plus a repo-level watcher for docs/** changes ``` @@ -93,7 +93,7 @@ Fern groups endpoints by their OpenAPI tag in the sidebar (Customizer, Evaluator Some features are not shipped yet and must be **fully excluded from the build** — not just hidden from the sidebar. Fern's `hidden: true` still builds and serves the page (reachable by direct URL and indexable), so it is **not** used for this. Instead, the gated pages are simply **left out of `versions/latest.yml`**: Fern only builds pages referenced in the navigation, so an omitted page is never built (it 404s and is not indexed). This matches the old MkDocs `hide_unready_docs` hook, which dropped the same files from the build. -The gated `.mdx` files stay in the repo so they remain maintained. The gated trees today are: `auth/`, `customizer/`, `safe-synthesizer/`, `evaluator/benchmarks/`, plus individual pages (`evaluator/metrics/{job-management,results}`, `run-inference/tutorials/deploy-models`, `example-applications/`, `troubleshooting/{cluster-setup,customizer}`, `get-started/quickstart`). +Gated `.mdx` files stay in the repo so they remain maintained. Do not keep a separate list of gated directories in contributor docs: publication state is derived from `versions/latest.yml`. A page listed there is published; an omitted page is gated. Inbound links from visible pages into gated pages are **delinked to plain text** (not rewritten URLs), since the target is not built — otherwise they would be broken links. @@ -114,7 +114,7 @@ One difference from the old MkDocs hook: that hook ran at build time and kept th | Workflow | Trigger | Purpose | | --- | --- | --- | -| `fern-docs-ci.yaml` | `pull_request` touching `docs/**` | `npm run check` (fern check + MDX + gated-link check) and `npm run broken-links` | +| `fern-docs-ci.yaml` | `pull_request` touching `docs/**` | `npm run check` (fern check + MDX + NotebookViewer artifacts + gated links) and `npm run broken-links` | | `fern-docs-preview-build.yaml` | `pull_request` touching `docs/**` | Upload PR `docs/` sources as an artifact (no secrets — fork-safe) | | `fern-docs-preview-comment.yaml` | successful preview build (`workflow_run`) | Build a Fern preview with `DOCS_FERN_TOKEN` and post/update the PR comment | | `publish-fern-docs.yaml` | push to `main` touching `docs/**`, `docs/v*` tag, or manual dispatch | Publish the Fern docs site | diff --git a/docs/fern/components/notebooks/distillation-customization-job.json b/docs/fern/components/notebooks/distillation-customization-job.json index 969cdfa323..3ae0a8e082 100644 --- a/docs/fern/components/notebooks/distillation-customization-job.json +++ b/docs/fern/components/notebooks/distillation-customization-job.json @@ -7,8 +7,8 @@ }, { "type": "markdown", - "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **Installed evaluation dependencies:**\n\n```sh\npip install evaluate rouge_score datasets\n```", - "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. Installed evaluation dependencies:
  6. \n
\n
pip install evaluate rouge_score datasets\n
\n" + "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **Installed evaluation dependencies:**\n\n```sh\npip install evaluate rouge_score datasets\n```\n\n4. **At least one GPU with CUDA 13+**", + "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. Installed evaluation dependencies:
  6. \n
\n
pip install evaluate rouge_score datasets\n
\n
    \n
  1. At least one GPU with CUDA 13+
  2. \n
\n" }, { "type": "markdown", @@ -40,8 +40,8 @@ }, { "type": "markdown", - "source": "### 3. Secrets Setup\n\nIn this tutorial we use two Llama 3.2 Instruct models from HuggingFace:\n- **Teacher:** [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct) (3B parameters)\n- **Student:** [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) (1B parameters)\n\nBoth models share the same tokenizer/vocabulary (required for knowledge distillation) and include a chat template for deployment with `/chat/completions`.\n\n**HuggingFace Authentication:**\n- For gated models (Llama, Gemma), you must provide a HuggingFace token via the `token_secret` parameter\n- Get your token from [HuggingFace Settings](https://huggingface.co/settings/tokens) (requires Read access)\n- Accept the model's terms on the HuggingFace model page before using it:\n - [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)\n - [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct)", - "source_html": "

3. Secrets Setup

\n

In this tutorial we use two Llama 3.2 Instruct models from HuggingFace:

\n\n

Both models share the same tokenizer/vocabulary (required for knowledge distillation) and include a chat template for deployment with /chat/completions.

\n

HuggingFace Authentication:

\n\n" + "source": "### 3. Secrets Setup\n\nIn this tutorial we use two Llama 3.2 Instruct models from Hugging Face:\n- **Teacher:** [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct) (3B parameters)\n- **Student:** [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) (1B parameters)\n\nBoth models share the same tokenizer/vocabulary (required for knowledge distillation) and include a chat template for deployment with `/chat/completions`.\n\n**Hugging Face Authentication:**\n- For gated models (Llama, Gemma), you must provide a Hugging Face token via the `token_secret` parameter\n- Get your token from [Hugging Face Settings](https://huggingface.co/settings/tokens) (requires Read access)\n- Accept the model's terms on the Hugging Face model page before using it:\n - [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)\n - [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct)", + "source_html": "

3. Secrets Setup

\n

In this tutorial we use two Llama 3.2 Instruct models from Hugging Face:

\n\n

Both models share the same tokenizer/vocabulary (required for knowledge distillation) and include a chat template for deployment with /chat/completions.

\n

Hugging Face Authentication:

\n\n" }, { "type": "code", @@ -90,9 +90,9 @@ }, { "type": "code", - "source": "def wait_for_deployment(deployment_name: str, timeout_minutes: int = 30):\n \"\"\"Poll deployment until ready.\"\"\"\n start = time.time()\n timeout = timeout_minutes * 60\n while True:\n dep = client.inference.deployments.retrieve(name=deployment_name, workspace=\"default\")\n elapsed = time.time() - start\n clear_output(wait=True)\n print(f\"Deployment: {deployment_name}\")\n print(f\"Status: {dep.status}\")\n print(f\"Elapsed: {int(elapsed // 60)}m {int(elapsed % 60)}s\")\n\n if dep.status == \"READY\":\n print(\"\\nDeployment is ready!\")\n return dep\n if dep.status in (\"FAILED\", \"ERROR\", \"TERMINATED\", \"LOST\"):\n print(f\"\\nDeployment failed: {dep.status}\")\n return dep\n if elapsed > timeout:\n print(f\"\\nTimeout ({timeout_minutes}m). Check status manually.\")\n return dep\n time.sleep(15)\n\n\ndep_status = wait_for_deployment(BASELINE_DEPLOYMENT_NAME)\nassert dep_status.status == \"READY\"", + "source": "def wait_for_deployment(deployment_name: str, timeout_minutes: int = 30):\n \"\"\"Poll deployment until ready.\"\"\"\n start = time.time()\n timeout = timeout_minutes * 60\n while True:\n dep = client.inference.deployments.retrieve(name=deployment_name, workspace=\"default\")\n elapsed = time.time() - start\n clear_output(wait=True)\n print(f\"Deployment: {deployment_name}\")\n print(f\"Status: {dep.status}\")\n print(f\"Elapsed: {int(elapsed // 60)}m {int(elapsed % 60)}s\")\n\n if dep.status == \"READY\":\n print(\"\\nDeployment is ready!\")\n remaining = int(timeout - elapsed)\n if remaining <= 0:\n raise TimeoutError(f\"Deployment timeout after {timeout_minutes} minutes\")\n if not client.models.wait_for_status(\n deployment_name=deployment_name,\n desired_status=\"READY\",\n workspace=\"default\",\n timeout=remaining,\n check_gateway=True,\n ):\n raise TimeoutError(\"Inference gateway did not become ready\")\n return dep\n if dep.status in (\"FAILED\", \"ERROR\", \"TERMINATED\", \"LOST\"):\n raise RuntimeError(f\"Deployment failed with status: {dep.status}\")\n if elapsed > timeout:\n raise TimeoutError(f\"Deployment timeout after {timeout_minutes} minutes\")\n time.sleep(15)\n\n\ntry:\n dep_status = wait_for_deployment(BASELINE_DEPLOYMENT_NAME)\n assert dep_status.status == \"READY\"\nexcept Exception:\n # Free GPUs if readiness fails before the later baseline-cleanup cell runs.\n try:\n client.inference.deployments.delete(name=BASELINE_DEPLOYMENT_NAME, workspace=\"default\")\n if not client.models.wait_for_status(\n deployment_name=BASELINE_DEPLOYMENT_NAME,\n desired_status=\"DELETED\",\n workspace=\"default\",\n timeout=600,\n ):\n raise TimeoutError(\n f\"Deployment {BASELINE_DEPLOYMENT_NAME} was not deleted within timeout\"\n )\n client.inference.deployment_configs.delete(name=BASELINE_DEPLOYMENT_CONFIG, workspace=\"default\")\n except Exception as cleanup_error:\n print(f\"Baseline cleanup after readiness failure also failed: {cleanup_error}\")\n raise", "language": "python", - "source_html": "def wait_for_deployment(deployment_name: str, timeout_minutes: int = 30):\n """Poll deployment until ready."""\n start = time.time()\n timeout = timeout_minutes * 60\n while True:\n dep = client.inference.deployments.retrieve(name=deployment_name, workspace="default")\n elapsed = time.time() - start\n clear_output(wait=True)\n print(f"Deployment: {deployment_name}")\n print(f"Status: {dep.status}")\n print(f"Elapsed: {int(elapsed // 60)}m {int(elapsed % 60)}s")\n\n if dep.status == "READY":\n print("\\nDeployment is ready!")\n return dep\n if dep.status in ("FAILED", "ERROR", "TERMINATED", "LOST"):\n print(f"\\nDeployment failed: {dep.status}")\n return dep\n if elapsed > timeout:\n print(f"\\nTimeout ({timeout_minutes}m). Check status manually.")\n return dep\n time.sleep(15)\n\n\ndep_status = wait_for_deployment(BASELINE_DEPLOYMENT_NAME)\nassert dep_status.status == "READY"\n" + "source_html": "def wait_for_deployment(deployment_name: str, timeout_minutes: int = 30):\n """Poll deployment until ready."""\n start = time.time()\n timeout = timeout_minutes * 60\n while True:\n dep = client.inference.deployments.retrieve(name=deployment_name, workspace="default")\n elapsed = time.time() - start\n clear_output(wait=True)\n print(f"Deployment: {deployment_name}")\n print(f"Status: {dep.status}")\n print(f"Elapsed: {int(elapsed // 60)}m {int(elapsed % 60)}s")\n\n if dep.status == "READY":\n print("\\nDeployment is ready!")\n remaining = int(timeout - elapsed)\n if remaining <= 0:\n raise TimeoutError(f"Deployment timeout after {timeout_minutes} minutes")\n if not client.models.wait_for_status(\n deployment_name=deployment_name,\n desired_status="READY",\n workspace="default",\n timeout=remaining,\n check_gateway=True,\n ):\n raise TimeoutError("Inference gateway did not become ready")\n return dep\n if dep.status in ("FAILED", "ERROR", "TERMINATED", "LOST"):\n raise RuntimeError(f"Deployment failed with status: {dep.status}")\n if elapsed > timeout:\n raise TimeoutError(f"Deployment timeout after {timeout_minutes} minutes")\n time.sleep(15)\n\n\ntry:\n dep_status = wait_for_deployment(BASELINE_DEPLOYMENT_NAME)\n assert dep_status.status == "READY"\nexcept Exception:\n # Free GPUs if readiness fails before the later baseline-cleanup cell runs.\n try:\n client.inference.deployments.delete(name=BASELINE_DEPLOYMENT_NAME, workspace="default")\n if not client.models.wait_for_status(\n deployment_name=BASELINE_DEPLOYMENT_NAME,\n desired_status="DELETED",\n workspace="default",\n timeout=600,\n ):\n raise TimeoutError(\n f"Deployment {BASELINE_DEPLOYMENT_NAME} was not deleted within timeout"\n )\n client.inference.deployment_configs.delete(name=BASELINE_DEPLOYMENT_CONFIG, workspace="default")\n except Exception as cleanup_error:\n print(f"Baseline cleanup after readiness failure also failed: {cleanup_error}")\n raise\n" }, { "type": "markdown", @@ -196,8 +196,8 @@ }, { "type": "markdown", - "source": "**Interpreting ROUGE Scores:**\n\n| Metric | Measures |\n|--------|----------|\n| **ROUGE-1** | Unigram overlap between prediction and reference |\n| **ROUGE-2** | Bigram overlap (captures phrase-level similarity) |\n| **ROUGE-L** | Longest common subsequence (captures sentence structure) |\n| **ROUGE-Lsum** | ROUGE-L computed over full summaries |\n\n**What to expect:**\n- The base student (1B, no training) provides a lower bound since it has not seen the task data\n- The distilled student (1B, KD) should significantly outperform the base student, demonstrating the knowledge transferred from the 3B teacher\n- If the distilled student scores are not much higher than the baseline, try increasing `distillation_temperature`, adjusting `distillation_ratio`, or training for more epochs\n\n---\n\n## Hyperparameters\n\nFor detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the [Hyperparameter Reference](../manage-customization-jobs/hyperparameters.md).\n\n---\n\n## Troubleshooting\n\n**Job fails during model download:**\n- Verify authentication secrets are configured (refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md))\n- For gated HuggingFace models (Llama, Gemma), accept the license on the model page\n- Check both `model` (student) and `teacher_model` URNs are correct\n- Ensure both model entities exist: `client.models.retrieve(name=..., workspace=\"default\")`\n\n**Job fails with OOM (Out of Memory) error:**\n\nKD loads both models, so OOM is more likely than with SFT:\n1. **First try:** Use `teacher_precision=\"bf16\"` to reduce teacher memory\n2. **Still OOM:** Reduce `micro_batch_size` to 1\n3. **Still OOM:** Reduce `global_batch_size` and `max_seq_length`\n4. **Last resort:** Increase `num_gpus_per_node`\n\n**No chat template / `/chat/completions` fails:**\n- Use Instruct model variants (e.g., `Llama-3.2-1B-Instruct`) instead of base models (`Llama-3.2-1B`). Base models do not include a chat template in their tokenizer, so the output model will also lack one.\n\n**Distilled model quality is poor:**\n- Increase `distillation_temperature` (try 2.0–5.0) to transfer more nuanced knowledge\n- Adjust `distillation_ratio`—if dataset labels are high-quality, lower the ratio; if the teacher is strong, raise it\n- Increase `epochs` or `max_steps` for more training\n- Verify teacher and student share the same vocabulary\n\n**Vocabulary mismatch error:**\n- Teacher and student must use the same tokenizer. Use models from the same family (e.g., Llama 3.2 1B Instruct + Llama 3.2 3B Instruct)\n\n**Deployment fails:**\n- Verify output model exists: `client.models.retrieve(name=DISTILLED_STUDENT_NAME, workspace=\"default\")`\n- Check deployment logs: `client.inference.deployments.get_logs(name=deployment.name, workspace=\"default\")`\n- The distilled model has the same size as the student, so GPU requirements match the student model\n\n\n## Next Steps\n\n- [Monitor training metrics](fine-tune-metrics) in detail\n- [Evaluate your fine-tuned model](../../evaluator/index) using the Evaluator service\n- Learn about [LoRA customization](./lora-customization-job) for resource-efficient fine-tuning\n- Learn about [Full SFT](./sft-customization-job) for direct supervised fine-tuning", - "source_html": "

Interpreting ROUGE Scores:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
MetricMeasures
ROUGE-1Unigram overlap between prediction and reference
ROUGE-2Bigram overlap (captures phrase-level similarity)
ROUGE-LLongest common subsequence (captures sentence structure)
ROUGE-LsumROUGE-L computed over full summaries
\n

What to expect:

\n
    \n
  • The base student (1B, no training) provides a lower bound since it has not seen the task data
  • \n
  • The distilled student (1B, KD) should significantly outperform the base student, demonstrating the knowledge transferred from the 3B teacher
  • \n
  • If the distilled student scores are not much higher than the baseline, try increasing distillation_temperature, adjusting distillation_ratio, or training for more epochs
  • \n
\n
\n

Hyperparameters

\n

For detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the Hyperparameter Reference.

\n
\n

Troubleshooting

\n

Job fails during model download:

\n
    \n
  • Verify authentication secrets are configured (refer to Managing Secrets)
  • \n
  • For gated HuggingFace models (Llama, Gemma), accept the license on the model page
  • \n
  • Check both model (student) and teacher_model URNs are correct
  • \n
  • Ensure both model entities exist: client.models.retrieve(name=..., workspace="default")
  • \n
\n

Job fails with OOM (Out of Memory) error:

\n

KD loads both models, so OOM is more likely than with SFT:

\n
    \n
  1. First try: Use teacher_precision="bf16" to reduce teacher memory
  2. \n
  3. Still OOM: Reduce micro_batch_size to 1
  4. \n
  5. Still OOM: Reduce global_batch_size and max_seq_length
  6. \n
  7. Last resort: Increase num_gpus_per_node
  8. \n
\n

No chat template / /chat/completions fails:

\n
    \n
  • Use Instruct model variants (e.g., Llama-3.2-1B-Instruct) instead of base models (Llama-3.2-1B). Base models do not include a chat template in their tokenizer, so the output model will also lack one.
  • \n
\n

Distilled model quality is poor:

\n
    \n
  • Increase distillation_temperature (try 2.0–5.0) to transfer more nuanced knowledge
  • \n
  • Adjust distillation_ratio—if dataset labels are high-quality, lower the ratio; if the teacher is strong, raise it
  • \n
  • Increase epochs or max_steps for more training
  • \n
  • Verify teacher and student share the same vocabulary
  • \n
\n

Vocabulary mismatch error:

\n
    \n
  • Teacher and student must use the same tokenizer. Use models from the same family (e.g., Llama 3.2 1B Instruct + Llama 3.2 3B Instruct)
  • \n
\n

Deployment fails:

\n
    \n
  • Verify output model exists: client.models.retrieve(name=DISTILLED_STUDENT_NAME, workspace="default")
  • \n
  • Check deployment logs: client.inference.deployments.get_logs(name=deployment.name, workspace="default")
  • \n
  • The distilled model has the same size as the student, so GPU requirements match the student model
  • \n
\n

Next Steps

\n\n" + "source": "**Interpreting ROUGE Scores:**\n\n| Metric | Measures |\n|--------|----------|\n| **ROUGE-1** | Unigram overlap between prediction and reference |\n| **ROUGE-2** | Bigram overlap (captures phrase-level similarity) |\n| **ROUGE-L** | Longest common subsequence (captures sentence structure) |\n| **ROUGE-Lsum** | ROUGE-L computed over full summaries |\n\n**What to expect:**\n- The base student (1B, no training) provides a lower bound since it has not seen the task data\n- The distilled student (1B, KD) should significantly outperform the base student, demonstrating the knowledge transferred from the 3B teacher\n- If the distilled student scores are not much higher than the baseline, try increasing `distillation_temperature`, adjusting `distillation_ratio`, or training for more epochs\n\n---\n\n## Hyperparameters\n\nFor detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the [Hyperparameter Reference](../manage-customization-jobs/hyperparameters.md).\n\n---\n\n## Troubleshooting\n\n**Job fails during model download:**\n- Verify authentication secrets are configured (refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md))\n- For gated Hugging Face models (Llama, Gemma), accept the license on the model page\n- Check both `model` (student) and `teacher_model` URNs are correct\n- Ensure both model entities exist: `client.models.retrieve(name=..., workspace=\"default\")`\n\n**Job fails with OOM (Out of Memory) error:**\n\nKD loads both models, so OOM is more likely than with SFT:\n1. **First try:** Use `teacher_precision=\"bf16\"` to reduce teacher memory\n2. **Still OOM:** Reduce `micro_batch_size` to 1\n3. **Still OOM:** Reduce `global_batch_size` and `max_seq_length`\n4. **Last resort:** Increase `num_gpus_per_node`\n\n**No chat template / `/chat/completions` fails:**\n- Use Instruct model variants (e.g., `Llama-3.2-1B-Instruct`) instead of base models (`Llama-3.2-1B`). Base models do not include a chat template in their tokenizer, so the output model will also lack one.\n\n**Distilled model quality is poor:**\n- Increase `distillation_temperature` (try 2.0–5.0) to transfer more nuanced knowledge\n- Adjust `distillation_ratio`—if dataset labels are high-quality, lower the ratio; if the teacher is strong, raise it\n- Increase `epochs` or `max_steps` for more training\n- Verify teacher and student share the same vocabulary\n\n**Vocabulary mismatch error:**\n- Teacher and student must use the same tokenizer. Use models from the same family (e.g., Llama 3.2 1B Instruct + Llama 3.2 3B Instruct)\n\n**Deployment fails:**\n- Verify output model exists: `client.models.retrieve(name=DISTILLED_STUDENT_NAME, workspace=\"default\")`\n- Check deployment logs: `client.inference.deployments.get_logs(name=deployment.name, workspace=\"default\")`\n- The distilled model has the same size as the student, so GPU requirements match the student model\n\n\n## Next Steps\n\n- [Monitor training metrics](fine-tune-metrics) in detail\n- [Evaluate your fine-tuned model](../../evaluator/index) using the Evaluator service\n- Learn about [LoRA customization](./lora-customization-job) for resource-efficient fine-tuning\n- Learn about [Full SFT](./sft-customization-job) for direct supervised fine-tuning", + "source_html": "

Interpreting ROUGE Scores:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
MetricMeasures
ROUGE-1Unigram overlap between prediction and reference
ROUGE-2Bigram overlap (captures phrase-level similarity)
ROUGE-LLongest common subsequence (captures sentence structure)
ROUGE-LsumROUGE-L computed over full summaries
\n

What to expect:

\n
    \n
  • The base student (1B, no training) provides a lower bound since it has not seen the task data
  • \n
  • The distilled student (1B, KD) should significantly outperform the base student, demonstrating the knowledge transferred from the 3B teacher
  • \n
  • If the distilled student scores are not much higher than the baseline, try increasing distillation_temperature, adjusting distillation_ratio, or training for more epochs
  • \n
\n
\n

Hyperparameters

\n

For detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the Hyperparameter Reference.

\n
\n

Troubleshooting

\n

Job fails during model download:

\n
    \n
  • Verify authentication secrets are configured (refer to Managing Secrets)
  • \n
  • For gated Hugging Face models (Llama, Gemma), accept the license on the model page
  • \n
  • Check both model (student) and teacher_model URNs are correct
  • \n
  • Ensure both model entities exist: client.models.retrieve(name=..., workspace="default")
  • \n
\n

Job fails with OOM (Out of Memory) error:

\n

KD loads both models, so OOM is more likely than with SFT:

\n
    \n
  1. First try: Use teacher_precision="bf16" to reduce teacher memory
  2. \n
  3. Still OOM: Reduce micro_batch_size to 1
  4. \n
  5. Still OOM: Reduce global_batch_size and max_seq_length
  6. \n
  7. Last resort: Increase num_gpus_per_node
  8. \n
\n

No chat template / /chat/completions fails:

\n
    \n
  • Use Instruct model variants (e.g., Llama-3.2-1B-Instruct) instead of base models (Llama-3.2-1B). Base models do not include a chat template in their tokenizer, so the output model will also lack one.
  • \n
\n

Distilled model quality is poor:

\n
    \n
  • Increase distillation_temperature (try 2.0–5.0) to transfer more nuanced knowledge
  • \n
  • Adjust distillation_ratio—if dataset labels are high-quality, lower the ratio; if the teacher is strong, raise it
  • \n
  • Increase epochs or max_steps for more training
  • \n
  • Verify teacher and student share the same vocabulary
  • \n
\n

Vocabulary mismatch error:

\n
    \n
  • Teacher and student must use the same tokenizer. Use models from the same family (e.g., Llama 3.2 1B Instruct + Llama 3.2 3B Instruct)
  • \n
\n

Deployment fails:

\n
    \n
  • Verify output model exists: client.models.retrieve(name=DISTILLED_STUDENT_NAME, workspace="default")
  • \n
  • Check deployment logs: client.inference.deployments.get_logs(name=deployment.name, workspace="default")
  • \n
  • The distilled model has the same size as the student, so GPU requirements match the student model
  • \n
\n

Next Steps

\n\n" } ] } \ No newline at end of file diff --git a/docs/fern/components/notebooks/distillation-customization-job.ts b/docs/fern/components/notebooks/distillation-customization-job.ts index 5190b821cd..83d6f3a29e 100644 --- a/docs/fern/components/notebooks/distillation-customization-job.ts +++ b/docs/fern/components/notebooks/distillation-customization-job.ts @@ -12,8 +12,8 @@ export default { cells: [ }, { "type": "markdown", - "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **Installed evaluation dependencies:**\n\n```sh\npip install evaluate rouge_score datasets\n```", - "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. Installed evaluation dependencies:
  6. \n
\n
pip install evaluate rouge_score datasets\n
\n" + "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **Installed evaluation dependencies:**\n\n```sh\npip install evaluate rouge_score datasets\n```\n\n4. **At least one GPU with CUDA 13+**", + "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. Installed evaluation dependencies:
  6. \n
\n
pip install evaluate rouge_score datasets\n
\n
    \n
  1. At least one GPU with CUDA 13+
  2. \n
\n" }, { "type": "markdown", @@ -45,8 +45,8 @@ export default { cells: [ }, { "type": "markdown", - "source": "### 3. Secrets Setup\n\nIn this tutorial we use two Llama 3.2 Instruct models from HuggingFace:\n- **Teacher:** [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct) (3B parameters)\n- **Student:** [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) (1B parameters)\n\nBoth models share the same tokenizer/vocabulary (required for knowledge distillation) and include a chat template for deployment with `/chat/completions`.\n\n**HuggingFace Authentication:**\n- For gated models (Llama, Gemma), you must provide a HuggingFace token via the `token_secret` parameter\n- Get your token from [HuggingFace Settings](https://huggingface.co/settings/tokens) (requires Read access)\n- Accept the model's terms on the HuggingFace model page before using it:\n - [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)\n - [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct)", - "source_html": "

3. Secrets Setup

\n

In this tutorial we use two Llama 3.2 Instruct models from HuggingFace:

\n\n

Both models share the same tokenizer/vocabulary (required for knowledge distillation) and include a chat template for deployment with /chat/completions.

\n

HuggingFace Authentication:

\n\n" + "source": "### 3. Secrets Setup\n\nIn this tutorial we use two Llama 3.2 Instruct models from Hugging Face:\n- **Teacher:** [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct) (3B parameters)\n- **Student:** [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) (1B parameters)\n\nBoth models share the same tokenizer/vocabulary (required for knowledge distillation) and include a chat template for deployment with `/chat/completions`.\n\n**Hugging Face Authentication:**\n- For gated models (Llama, Gemma), you must provide a Hugging Face token via the `token_secret` parameter\n- Get your token from [Hugging Face Settings](https://huggingface.co/settings/tokens) (requires Read access)\n- Accept the model's terms on the Hugging Face model page before using it:\n - [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)\n - [meta-llama/Llama-3.2-3B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-3B-Instruct)", + "source_html": "

3. Secrets Setup

\n

In this tutorial we use two Llama 3.2 Instruct models from Hugging Face:

\n\n

Both models share the same tokenizer/vocabulary (required for knowledge distillation) and include a chat template for deployment with /chat/completions.

\n

Hugging Face Authentication:

\n\n" }, { "type": "code", @@ -95,9 +95,9 @@ export default { cells: [ }, { "type": "code", - "source": "def wait_for_deployment(deployment_name: str, timeout_minutes: int = 30):\n \"\"\"Poll deployment until ready.\"\"\"\n start = time.time()\n timeout = timeout_minutes * 60\n while True:\n dep = client.inference.deployments.retrieve(name=deployment_name, workspace=\"default\")\n elapsed = time.time() - start\n clear_output(wait=True)\n print(f\"Deployment: {deployment_name}\")\n print(f\"Status: {dep.status}\")\n print(f\"Elapsed: {int(elapsed // 60)}m {int(elapsed % 60)}s\")\n\n if dep.status == \"READY\":\n print(\"\\nDeployment is ready!\")\n return dep\n if dep.status in (\"FAILED\", \"ERROR\", \"TERMINATED\", \"LOST\"):\n print(f\"\\nDeployment failed: {dep.status}\")\n return dep\n if elapsed > timeout:\n print(f\"\\nTimeout ({timeout_minutes}m). Check status manually.\")\n return dep\n time.sleep(15)\n\n\ndep_status = wait_for_deployment(BASELINE_DEPLOYMENT_NAME)\nassert dep_status.status == \"READY\"", + "source": "def wait_for_deployment(deployment_name: str, timeout_minutes: int = 30):\n \"\"\"Poll deployment until ready.\"\"\"\n start = time.time()\n timeout = timeout_minutes * 60\n while True:\n dep = client.inference.deployments.retrieve(name=deployment_name, workspace=\"default\")\n elapsed = time.time() - start\n clear_output(wait=True)\n print(f\"Deployment: {deployment_name}\")\n print(f\"Status: {dep.status}\")\n print(f\"Elapsed: {int(elapsed // 60)}m {int(elapsed % 60)}s\")\n\n if dep.status == \"READY\":\n print(\"\\nDeployment is ready!\")\n remaining = int(timeout - elapsed)\n if remaining <= 0:\n raise TimeoutError(f\"Deployment timeout after {timeout_minutes} minutes\")\n if not client.models.wait_for_status(\n deployment_name=deployment_name,\n desired_status=\"READY\",\n workspace=\"default\",\n timeout=remaining,\n check_gateway=True,\n ):\n raise TimeoutError(\"Inference gateway did not become ready\")\n return dep\n if dep.status in (\"FAILED\", \"ERROR\", \"TERMINATED\", \"LOST\"):\n raise RuntimeError(f\"Deployment failed with status: {dep.status}\")\n if elapsed > timeout:\n raise TimeoutError(f\"Deployment timeout after {timeout_minutes} minutes\")\n time.sleep(15)\n\n\ntry:\n dep_status = wait_for_deployment(BASELINE_DEPLOYMENT_NAME)\n assert dep_status.status == \"READY\"\nexcept Exception:\n # Free GPUs if readiness fails before the later baseline-cleanup cell runs.\n try:\n client.inference.deployments.delete(name=BASELINE_DEPLOYMENT_NAME, workspace=\"default\")\n if not client.models.wait_for_status(\n deployment_name=BASELINE_DEPLOYMENT_NAME,\n desired_status=\"DELETED\",\n workspace=\"default\",\n timeout=600,\n ):\n raise TimeoutError(\n f\"Deployment {BASELINE_DEPLOYMENT_NAME} was not deleted within timeout\"\n )\n client.inference.deployment_configs.delete(name=BASELINE_DEPLOYMENT_CONFIG, workspace=\"default\")\n except Exception as cleanup_error:\n print(f\"Baseline cleanup after readiness failure also failed: {cleanup_error}\")\n raise", "language": "python", - "source_html": "def wait_for_deployment(deployment_name: str, timeout_minutes: int = 30):\n """Poll deployment until ready."""\n start = time.time()\n timeout = timeout_minutes * 60\n while True:\n dep = client.inference.deployments.retrieve(name=deployment_name, workspace="default")\n elapsed = time.time() - start\n clear_output(wait=True)\n print(f"Deployment: {deployment_name}")\n print(f"Status: {dep.status}")\n print(f"Elapsed: {int(elapsed // 60)}m {int(elapsed % 60)}s")\n\n if dep.status == "READY":\n print("\\nDeployment is ready!")\n return dep\n if dep.status in ("FAILED", "ERROR", "TERMINATED", "LOST"):\n print(f"\\nDeployment failed: {dep.status}")\n return dep\n if elapsed > timeout:\n print(f"\\nTimeout ({timeout_minutes}m). Check status manually.")\n return dep\n time.sleep(15)\n\n\ndep_status = wait_for_deployment(BASELINE_DEPLOYMENT_NAME)\nassert dep_status.status == "READY"\n" + "source_html": "def wait_for_deployment(deployment_name: str, timeout_minutes: int = 30):\n """Poll deployment until ready."""\n start = time.time()\n timeout = timeout_minutes * 60\n while True:\n dep = client.inference.deployments.retrieve(name=deployment_name, workspace="default")\n elapsed = time.time() - start\n clear_output(wait=True)\n print(f"Deployment: {deployment_name}")\n print(f"Status: {dep.status}")\n print(f"Elapsed: {int(elapsed // 60)}m {int(elapsed % 60)}s")\n\n if dep.status == "READY":\n print("\\nDeployment is ready!")\n remaining = int(timeout - elapsed)\n if remaining <= 0:\n raise TimeoutError(f"Deployment timeout after {timeout_minutes} minutes")\n if not client.models.wait_for_status(\n deployment_name=deployment_name,\n desired_status="READY",\n workspace="default",\n timeout=remaining,\n check_gateway=True,\n ):\n raise TimeoutError("Inference gateway did not become ready")\n return dep\n if dep.status in ("FAILED", "ERROR", "TERMINATED", "LOST"):\n raise RuntimeError(f"Deployment failed with status: {dep.status}")\n if elapsed > timeout:\n raise TimeoutError(f"Deployment timeout after {timeout_minutes} minutes")\n time.sleep(15)\n\n\ntry:\n dep_status = wait_for_deployment(BASELINE_DEPLOYMENT_NAME)\n assert dep_status.status == "READY"\nexcept Exception:\n # Free GPUs if readiness fails before the later baseline-cleanup cell runs.\n try:\n client.inference.deployments.delete(name=BASELINE_DEPLOYMENT_NAME, workspace="default")\n if not client.models.wait_for_status(\n deployment_name=BASELINE_DEPLOYMENT_NAME,\n desired_status="DELETED",\n workspace="default",\n timeout=600,\n ):\n raise TimeoutError(\n f"Deployment {BASELINE_DEPLOYMENT_NAME} was not deleted within timeout"\n )\n client.inference.deployment_configs.delete(name=BASELINE_DEPLOYMENT_CONFIG, workspace="default")\n except Exception as cleanup_error:\n print(f"Baseline cleanup after readiness failure also failed: {cleanup_error}")\n raise\n" }, { "type": "markdown", @@ -201,7 +201,7 @@ export default { cells: [ }, { "type": "markdown", - "source": "**Interpreting ROUGE Scores:**\n\n| Metric | Measures |\n|--------|----------|\n| **ROUGE-1** | Unigram overlap between prediction and reference |\n| **ROUGE-2** | Bigram overlap (captures phrase-level similarity) |\n| **ROUGE-L** | Longest common subsequence (captures sentence structure) |\n| **ROUGE-Lsum** | ROUGE-L computed over full summaries |\n\n**What to expect:**\n- The base student (1B, no training) provides a lower bound since it has not seen the task data\n- The distilled student (1B, KD) should significantly outperform the base student, demonstrating the knowledge transferred from the 3B teacher\n- If the distilled student scores are not much higher than the baseline, try increasing `distillation_temperature`, adjusting `distillation_ratio`, or training for more epochs\n\n---\n\n## Hyperparameters\n\nFor detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the [Hyperparameter Reference](../manage-customization-jobs/hyperparameters.md).\n\n---\n\n## Troubleshooting\n\n**Job fails during model download:**\n- Verify authentication secrets are configured (refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md))\n- For gated HuggingFace models (Llama, Gemma), accept the license on the model page\n- Check both `model` (student) and `teacher_model` URNs are correct\n- Ensure both model entities exist: `client.models.retrieve(name=..., workspace=\"default\")`\n\n**Job fails with OOM (Out of Memory) error:**\n\nKD loads both models, so OOM is more likely than with SFT:\n1. **First try:** Use `teacher_precision=\"bf16\"` to reduce teacher memory\n2. **Still OOM:** Reduce `micro_batch_size` to 1\n3. **Still OOM:** Reduce `global_batch_size` and `max_seq_length`\n4. **Last resort:** Increase `num_gpus_per_node`\n\n**No chat template / `/chat/completions` fails:**\n- Use Instruct model variants (e.g., `Llama-3.2-1B-Instruct`) instead of base models (`Llama-3.2-1B`). Base models do not include a chat template in their tokenizer, so the output model will also lack one.\n\n**Distilled model quality is poor:**\n- Increase `distillation_temperature` (try 2.0–5.0) to transfer more nuanced knowledge\n- Adjust `distillation_ratio`—if dataset labels are high-quality, lower the ratio; if the teacher is strong, raise it\n- Increase `epochs` or `max_steps` for more training\n- Verify teacher and student share the same vocabulary\n\n**Vocabulary mismatch error:**\n- Teacher and student must use the same tokenizer. Use models from the same family (e.g., Llama 3.2 1B Instruct + Llama 3.2 3B Instruct)\n\n**Deployment fails:**\n- Verify output model exists: `client.models.retrieve(name=DISTILLED_STUDENT_NAME, workspace=\"default\")`\n- Check deployment logs: `client.inference.deployments.get_logs(name=deployment.name, workspace=\"default\")`\n- The distilled model has the same size as the student, so GPU requirements match the student model\n\n\n## Next Steps\n\n- [Monitor training metrics](fine-tune-metrics) in detail\n- [Evaluate your fine-tuned model](../../evaluator/index) using the Evaluator service\n- Learn about [LoRA customization](./lora-customization-job) for resource-efficient fine-tuning\n- Learn about [Full SFT](./sft-customization-job) for direct supervised fine-tuning", - "source_html": "

Interpreting ROUGE Scores:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
MetricMeasures
ROUGE-1Unigram overlap between prediction and reference
ROUGE-2Bigram overlap (captures phrase-level similarity)
ROUGE-LLongest common subsequence (captures sentence structure)
ROUGE-LsumROUGE-L computed over full summaries
\n

What to expect:

\n
    \n
  • The base student (1B, no training) provides a lower bound since it has not seen the task data
  • \n
  • The distilled student (1B, KD) should significantly outperform the base student, demonstrating the knowledge transferred from the 3B teacher
  • \n
  • If the distilled student scores are not much higher than the baseline, try increasing distillation_temperature, adjusting distillation_ratio, or training for more epochs
  • \n
\n
\n

Hyperparameters

\n

For detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the Hyperparameter Reference.

\n
\n

Troubleshooting

\n

Job fails during model download:

\n
    \n
  • Verify authentication secrets are configured (refer to Managing Secrets)
  • \n
  • For gated HuggingFace models (Llama, Gemma), accept the license on the model page
  • \n
  • Check both model (student) and teacher_model URNs are correct
  • \n
  • Ensure both model entities exist: client.models.retrieve(name=..., workspace="default")
  • \n
\n

Job fails with OOM (Out of Memory) error:

\n

KD loads both models, so OOM is more likely than with SFT:

\n
    \n
  1. First try: Use teacher_precision="bf16" to reduce teacher memory
  2. \n
  3. Still OOM: Reduce micro_batch_size to 1
  4. \n
  5. Still OOM: Reduce global_batch_size and max_seq_length
  6. \n
  7. Last resort: Increase num_gpus_per_node
  8. \n
\n

No chat template / /chat/completions fails:

\n
    \n
  • Use Instruct model variants (e.g., Llama-3.2-1B-Instruct) instead of base models (Llama-3.2-1B). Base models do not include a chat template in their tokenizer, so the output model will also lack one.
  • \n
\n

Distilled model quality is poor:

\n
    \n
  • Increase distillation_temperature (try 2.0–5.0) to transfer more nuanced knowledge
  • \n
  • Adjust distillation_ratio—if dataset labels are high-quality, lower the ratio; if the teacher is strong, raise it
  • \n
  • Increase epochs or max_steps for more training
  • \n
  • Verify teacher and student share the same vocabulary
  • \n
\n

Vocabulary mismatch error:

\n
    \n
  • Teacher and student must use the same tokenizer. Use models from the same family (e.g., Llama 3.2 1B Instruct + Llama 3.2 3B Instruct)
  • \n
\n

Deployment fails:

\n
    \n
  • Verify output model exists: client.models.retrieve(name=DISTILLED_STUDENT_NAME, workspace="default")
  • \n
  • Check deployment logs: client.inference.deployments.get_logs(name=deployment.name, workspace="default")
  • \n
  • The distilled model has the same size as the student, so GPU requirements match the student model
  • \n
\n

Next Steps

\n\n" + "source": "**Interpreting ROUGE Scores:**\n\n| Metric | Measures |\n|--------|----------|\n| **ROUGE-1** | Unigram overlap between prediction and reference |\n| **ROUGE-2** | Bigram overlap (captures phrase-level similarity) |\n| **ROUGE-L** | Longest common subsequence (captures sentence structure) |\n| **ROUGE-Lsum** | ROUGE-L computed over full summaries |\n\n**What to expect:**\n- The base student (1B, no training) provides a lower bound since it has not seen the task data\n- The distilled student (1B, KD) should significantly outperform the base student, demonstrating the knowledge transferred from the 3B teacher\n- If the distilled student scores are not much higher than the baseline, try increasing `distillation_temperature`, adjusting `distillation_ratio`, or training for more epochs\n\n---\n\n## Hyperparameters\n\nFor detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the [Hyperparameter Reference](../manage-customization-jobs/hyperparameters.md).\n\n---\n\n## Troubleshooting\n\n**Job fails during model download:**\n- Verify authentication secrets are configured (refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md))\n- For gated Hugging Face models (Llama, Gemma), accept the license on the model page\n- Check both `model` (student) and `teacher_model` URNs are correct\n- Ensure both model entities exist: `client.models.retrieve(name=..., workspace=\"default\")`\n\n**Job fails with OOM (Out of Memory) error:**\n\nKD loads both models, so OOM is more likely than with SFT:\n1. **First try:** Use `teacher_precision=\"bf16\"` to reduce teacher memory\n2. **Still OOM:** Reduce `micro_batch_size` to 1\n3. **Still OOM:** Reduce `global_batch_size` and `max_seq_length`\n4. **Last resort:** Increase `num_gpus_per_node`\n\n**No chat template / `/chat/completions` fails:**\n- Use Instruct model variants (e.g., `Llama-3.2-1B-Instruct`) instead of base models (`Llama-3.2-1B`). Base models do not include a chat template in their tokenizer, so the output model will also lack one.\n\n**Distilled model quality is poor:**\n- Increase `distillation_temperature` (try 2.0–5.0) to transfer more nuanced knowledge\n- Adjust `distillation_ratio`—if dataset labels are high-quality, lower the ratio; if the teacher is strong, raise it\n- Increase `epochs` or `max_steps` for more training\n- Verify teacher and student share the same vocabulary\n\n**Vocabulary mismatch error:**\n- Teacher and student must use the same tokenizer. Use models from the same family (e.g., Llama 3.2 1B Instruct + Llama 3.2 3B Instruct)\n\n**Deployment fails:**\n- Verify output model exists: `client.models.retrieve(name=DISTILLED_STUDENT_NAME, workspace=\"default\")`\n- Check deployment logs: `client.inference.deployments.get_logs(name=deployment.name, workspace=\"default\")`\n- The distilled model has the same size as the student, so GPU requirements match the student model\n\n\n## Next Steps\n\n- [Monitor training metrics](fine-tune-metrics) in detail\n- [Evaluate your fine-tuned model](../../evaluator/index) using the Evaluator service\n- Learn about [LoRA customization](./lora-customization-job) for resource-efficient fine-tuning\n- Learn about [Full SFT](./sft-customization-job) for direct supervised fine-tuning", + "source_html": "

Interpreting ROUGE Scores:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
MetricMeasures
ROUGE-1Unigram overlap between prediction and reference
ROUGE-2Bigram overlap (captures phrase-level similarity)
ROUGE-LLongest common subsequence (captures sentence structure)
ROUGE-LsumROUGE-L computed over full summaries
\n

What to expect:

\n
    \n
  • The base student (1B, no training) provides a lower bound since it has not seen the task data
  • \n
  • The distilled student (1B, KD) should significantly outperform the base student, demonstrating the knowledge transferred from the 3B teacher
  • \n
  • If the distilled student scores are not much higher than the baseline, try increasing distillation_temperature, adjusting distillation_ratio, or training for more epochs
  • \n
\n
\n

Hyperparameters

\n

For detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the Hyperparameter Reference.

\n
\n

Troubleshooting

\n

Job fails during model download:

\n
    \n
  • Verify authentication secrets are configured (refer to Managing Secrets)
  • \n
  • For gated Hugging Face models (Llama, Gemma), accept the license on the model page
  • \n
  • Check both model (student) and teacher_model URNs are correct
  • \n
  • Ensure both model entities exist: client.models.retrieve(name=..., workspace="default")
  • \n
\n

Job fails with OOM (Out of Memory) error:

\n

KD loads both models, so OOM is more likely than with SFT:

\n
    \n
  1. First try: Use teacher_precision="bf16" to reduce teacher memory
  2. \n
  3. Still OOM: Reduce micro_batch_size to 1
  4. \n
  5. Still OOM: Reduce global_batch_size and max_seq_length
  6. \n
  7. Last resort: Increase num_gpus_per_node
  8. \n
\n

No chat template / /chat/completions fails:

\n
    \n
  • Use Instruct model variants (e.g., Llama-3.2-1B-Instruct) instead of base models (Llama-3.2-1B). Base models do not include a chat template in their tokenizer, so the output model will also lack one.
  • \n
\n

Distilled model quality is poor:

\n
    \n
  • Increase distillation_temperature (try 2.0–5.0) to transfer more nuanced knowledge
  • \n
  • Adjust distillation_ratio—if dataset labels are high-quality, lower the ratio; if the teacher is strong, raise it
  • \n
  • Increase epochs or max_steps for more training
  • \n
  • Verify teacher and student share the same vocabulary
  • \n
\n

Vocabulary mismatch error:

\n
    \n
  • Teacher and student must use the same tokenizer. Use models from the same family (e.g., Llama 3.2 1B Instruct + Llama 3.2 3B Instruct)
  • \n
\n

Deployment fails:

\n
    \n
  • Verify output model exists: client.models.retrieve(name=DISTILLED_STUDENT_NAME, workspace="default")
  • \n
  • Check deployment logs: client.inference.deployments.get_logs(name=deployment.name, workspace="default")
  • \n
  • The distilled model has the same size as the student, so GPU requirements match the student model
  • \n
\n

Next Steps

\n\n" } ] }; diff --git a/docs/fern/components/notebooks/dpo-customization-job.json b/docs/fern/components/notebooks/dpo-customization-job.json new file mode 100644 index 0000000000..ab69dab155 --- /dev/null +++ b/docs/fern/components/notebooks/dpo-customization-job.json @@ -0,0 +1,155 @@ +{ + "cells": [ + { + "type": "markdown", + "source": "\n\n\n# DPO Model Customization Job\n\nLearn how to use the NeMo Platform to align a model with **DPO** (Direct Preference Optimization) on a preference dataset. For each prompt, DPO trains on a *chosen* (preferred) and a *rejected* response so the model prefers the chosen style — no separate reward model required.\n\nThis tutorial uses the `rl` customization backend (powered by [NVIDIA NeMo-RL](https://github.com/NVIDIA-NeMo/RL)), which runs DPO on a **Ray** cluster. Unlike the [SFT](/documentation/customizer-reference/tutorials/sft-customization-job) and [LoRA](/documentation/customizer-reference/tutorials/lora-customization-job) tutorials (Docker GPU jobs), `rl` requires a **Kubernetes-backed** NeMo Platform. DPO here is **full-weight** (no LoRA/adapter); the output is a full model entity.\n\n**Time to complete:** approximately 45-60 minutes. Job duration increases with model and dataset size.", + "source_html": "\n\n

DPO Model Customization Job

\n

Learn how to use the NeMo Platform to align a model with DPO (Direct Preference Optimization) on a preference dataset. For each prompt, DPO trains on a chosen (preferred) and a rejected response so the model prefers the chosen style — no separate reward model required.

\n

This tutorial uses the rl customization backend (powered by NVIDIA NeMo-RL), which runs DPO on a Ray cluster. Unlike the SFT and LoRA tutorials (Docker GPU jobs), rl requires a Kubernetes-backed NeMo Platform. DPO here is full-weight (no LoRA/adapter); the output is a full model entity.

\n

Time to complete: approximately 45-60 minutes. Job duration increases with model and dataset size.

\n" + }, + { + "type": "markdown", + "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](/documentation/get-started)** to install the NeMo Platform and Python SDK.\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root).\n3. **Installed the `datasets` package**: `pip install datasets`.\n4. **A platform configured with `platform.runtime: kubernetes`.** The `rl` (DPO) backend provisions a Ray cluster and has **no local Docker fallback** — `submit` fails fast on a Docker-runtime platform. Multi-node jobs (`parallelism.num_nodes > 1`) additionally require the platform-side `NMP_RL_MULTINODE_SHARED_STORAGE_PATH`.\n5. **A Hugging Face token** with access to the gated base model (this tutorial uses `meta-llama/Llama-3.2-1B-Instruct`). Export it as `HF_TOKEN`.\n6. **At least one GPU with CUDA 13+** and a GPU execution profile (`nemo jobs list-execution-profiles`).", + "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install the NeMo Platform and Python SDK.
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root).
  4. \n
  5. Installed the datasets package: pip install datasets.
  6. \n
  7. A platform configured with platform.runtime: kubernetes. The rl (DPO) backend provisions a Ray cluster and has no local Docker fallbacksubmit fails fast on a Docker-runtime platform. Multi-node jobs (parallelism.num_nodes > 1) additionally require the platform-side NMP_RL_MULTINODE_SHARED_STORAGE_PATH.
  8. \n
  9. A Hugging Face token with access to the gated base model (this tutorial uses meta-llama/Llama-3.2-1B-Instruct). Export it as HF_TOKEN.
  10. \n
  11. At least one GPU with CUDA 13+ and a GPU execution profile (nemo jobs list-execution-profiles).
  12. \n
\n" + }, + { + "type": "markdown", + "source": "## Quick Start\n\n### 1. Initialize the SDK\n\nThe SDK needs your NeMo Platform server URL. By default `http://localhost:8080` is used; set `NMP_BASE_URL` to override:\n\n```sh\nexport NMP_BASE_URL=\n```", + "source_html": "

Quick Start

\n

1. Initialize the SDK

\n

The SDK needs your NeMo Platform server URL. By default http://localhost:8080 is used; set NMP_BASE_URL to override:

\n
export NMP_BASE_URL=<YOUR_NMP_BASE_URL>\n
\n" + }, + { + "type": "code", + "source": "import json\nimport os\nimport time\nimport uuid\nfrom pathlib import Path\nfrom nemo_platform import NeMoPlatform, ConflictError\nfrom nemo_platform.types.secrets import PlatformSecretResponse\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\nfrom nemo_rl_plugin.schema import RlJobInput\n\n\ndef max_wait_time_checker(seconds: int, label: str = \"\"):\n \"\"\"Return a check() that raises TimeoutError once `seconds` have elapsed.\"\"\"\n start = time.time()\n\n def check():\n if time.time() - start > seconds:\n raise TimeoutError(f\"{label} took longer than {seconds} seconds\")\n\n return check\n\n\nNMP_BASE_URL = os.environ.get(\"NMP_BASE_URL\", \"http://localhost:8080\")\nsdk = NeMoPlatform(base_url=NMP_BASE_URL, workspace=\"default\")", + "language": "python", + "source_html": "import json\nimport os\nimport time\nimport uuid\nfrom pathlib import Path\nfrom nemo_platform import NeMoPlatform, ConflictError\nfrom nemo_platform.types.secrets import PlatformSecretResponse\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\nfrom nemo_rl_plugin.schema import RlJobInput\n\n\ndef max_wait_time_checker(seconds: int, label: str = ""):\n """Return a check() that raises TimeoutError once `seconds` have elapsed."""\n start = time.time()\n\n def check():\n if time.time() - start > seconds:\n raise TimeoutError(f"{label} took longer than {seconds} seconds")\n\n return check\n\n\nNMP_BASE_URL = os.environ.get("NMP_BASE_URL", "http://localhost:8080")\nsdk = NeMoPlatform(base_url=NMP_BASE_URL, workspace="default")\n" + }, + { + "type": "markdown", + "source": "### 2. Prepare the Preference Dataset\n\nDPO trains on **preference data**. The `rl` backend takes a **single** dataset fileset that holds both `training.jsonl` and `validation.jsonl`, and auto-detects the row schema from the first line. Three preference formats are supported (see the platform's `BinaryPreferenceDatasetItemSchema` / `HelpSteer3DatasetItemSchema` / `Tulu3PreferenceDatasetItemSchema`):", + "source_html": "

2. Prepare the Preference Dataset

\n

DPO trains on preference data. The rl backend takes a single dataset fileset that holds both training.jsonl and validation.jsonl, and auto-detects the row schema from the first line. Three preference formats are supported (see the platform's BinaryPreferenceDatasetItemSchema / HelpSteer3DatasetItemSchema / Tulu3PreferenceDatasetItemSchema):

\n" + }, + { + "type": "markdown", + "source": "#### Binary Preference Format\n\nSimple `prompt` / `chosen` / `rejected` (the `prompt` may be a string or a list of chat messages):\n\n```json\n{\"prompt\": \"What is the capital of France?\", \"chosen\": \"The capital of France is Paris.\", \"rejected\": \"I'm not sure.\"}\n```", + "source_html": "

Binary Preference Format

\n

Simple prompt / chosen / rejected (the prompt may be a string or a list of chat messages):

\n
{"prompt": "What is the capital of France?", "chosen": "The capital of France is Paris.", "rejected": "I'm not sure."}\n
\n" + }, + { + "type": "markdown", + "source": "#### HelpSteer3 Format (used here)\n\nA conversation `context` (string or chat messages), two candidate `response1` / `response2`, and a signed `overall_preference` in -3..3 — **negative** means response 1 is preferred, **positive** means response 2, **0** is a tie. This is the **raw** schema of `nvidia/HelpSteer3`, so no conversion is needed:\n\n```json\n{\"context\": [{\"role\": \"user\", \"content\": \"Explain how to use git rebase\"}], \"response1\": \"...\", \"response2\": \"...\", \"overall_preference\": -2}\n```", + "source_html": "

HelpSteer3 Format (used here)

\n

A conversation context (string or chat messages), two candidate response1 / response2, and a signed overall_preference in -3..3 — negative means response 1 is preferred, positive means response 2, 0 is a tie. This is the raw schema of nvidia/HelpSteer3, so no conversion is needed:

\n
{"context": [{"role": "user", "content": "Explain how to use git rebase"}], "response1": "...", "response2": "...", "overall_preference": -2}\n
\n" + }, + { + "type": "markdown", + "source": "#### Tulu3 Preference Format\n\nFull chat conversations for both the chosen and rejected branches (each a list of messages ending with the assistant turn):\n\n```json\n{\"chosen\": [{\"role\": \"user\", \"content\": \"...\"}, {\"role\": \"assistant\", \"content\": \"preferred\"}], \"rejected\": [{\"role\": \"user\", \"content\": \"...\"}, {\"role\": \"assistant\", \"content\": \"dispreferred\"}]}\n```", + "source_html": "

Tulu3 Preference Format

\n

Full chat conversations for both the chosen and rejected branches (each a list of messages ending with the assistant turn):

\n
{"chosen": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "preferred"}], "rejected": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "dispreferred"}]}\n
\n" + }, + { + "type": "markdown", + "source": "#### Download nvidia/HelpSteer3\n\nWe use [nvidia/HelpSteer3](https://huggingface.co/datasets/nvidia/HelpSteer3) (the `preference` subset), NVIDIA's open preference dataset. It ships native `train` and `validation` splits and matches the HelpSteer3 schema above, so we upload the rows **as-is** — the platform's `HelpSteer3Dataset` loader handles the `overall_preference` semantics (including ties) at training time.", + "source_html": "

Download nvidia/HelpSteer3

\n

We use nvidia/HelpSteer3 (the preference subset), NVIDIA's open preference dataset. It ships native train and validation splits and matches the HelpSteer3 schema above, so we upload the rows as-is — the platform's HelpSteer3Dataset loader handles the overall_preference semantics (including ties) at training time.

\n" + }, + { + "type": "code", + "source": "from datasets import load_dataset, Dataset\n\nprint(\"Loading dataset nvidia/HelpSteer3 (preference subset)\")\nds = load_dataset(\"nvidia/HelpSteer3\", \"preference\")\n\n# Small subsets keep the tutorial fast; larger sets train better but take longer.\ntraining_size = 3000\nvalidation_size = 300\nDATASET_NAME = \"dpo-dataset\"\nDATASET_PATH = Path(\"dpo-dataset\").absolute()\nos.makedirs(DATASET_PATH, exist_ok=True)\n\ntrain_dataset = ds[\"train\"]\nvalidation_dataset = ds[\"validation\"]\nassert isinstance(train_dataset, Dataset) and isinstance(validation_dataset, Dataset)\n\n# Save raw HelpSteer3 rows directly — no conversion. The platform detects the\n# HelpSteer3 schema from the row keys (context / response1 / response2 / overall_preference).\ntrain_dataset.select(range(training_size)).to_json(f\"{DATASET_PATH}/training.jsonl\")\nvalidation_dataset.select(range(validation_size)).to_json(f\"{DATASET_PATH}/validation.jsonl\")\n\nprint(f\"Saved training.jsonl ({training_size} rows) and validation.jsonl ({validation_size} rows)\")\nwith open(f\"{DATASET_PATH}/training.jsonl\") as f:\n sample = json.loads(f.readline())\nprint(\"Sample keys:\", sorted(sample.keys()))\nprint(\"overall_preference:\", sample[\"overall_preference\"])", + "language": "python", + "source_html": "from datasets import load_dataset, Dataset\n\nprint("Loading dataset nvidia/HelpSteer3 (preference subset)")\nds = load_dataset("nvidia/HelpSteer3", "preference")\n\n# Small subsets keep the tutorial fast; larger sets train better but take longer.\ntraining_size = 3000\nvalidation_size = 300\nDATASET_NAME = "dpo-dataset"\nDATASET_PATH = Path("dpo-dataset").absolute()\nos.makedirs(DATASET_PATH, exist_ok=True)\n\ntrain_dataset = ds["train"]\nvalidation_dataset = ds["validation"]\nassert isinstance(train_dataset, Dataset) and isinstance(validation_dataset, Dataset)\n\n# Save raw HelpSteer3 rows directly — no conversion. The platform detects the\n# HelpSteer3 schema from the row keys (context / response1 / response2 / overall_preference).\ntrain_dataset.select(range(training_size)).to_json(f"{DATASET_PATH}/training.jsonl")\nvalidation_dataset.select(range(validation_size)).to_json(f"{DATASET_PATH}/validation.jsonl")\n\nprint(f"Saved training.jsonl ({training_size} rows) and validation.jsonl ({validation_size} rows)")\nwith open(f"{DATASET_PATH}/training.jsonl") as f:\n sample = json.loads(f.readline())\nprint("Sample keys:", sorted(sample.keys()))\nprint("overall_preference:", sample["overall_preference"])\n" + }, + { + "type": "markdown", + "source": "### 3. Create FileSet and Upload Preference Data\n\nUpload both JSONL files to a single FileSet so the DPO job can read them.", + "source_html": "

3. Create FileSet and Upload Preference Data

\n

Upload both JSONL files to a single FileSet so the DPO job can read them.

\n" + }, + { + "type": "code", + "source": "try:\n sdk.files.filesets.create(workspace=\"default\", name=DATASET_NAME, description=\"DPO preference data\")\n print(f\"Created fileset: {DATASET_NAME}\")\nexcept ConflictError:\n print(f\"Fileset '{DATASET_NAME}' already exists, continuing...\")\n\nsdk.files.upload(local_path=DATASET_PATH, remote_path=\"\", fileset=DATASET_NAME, workspace=\"default\")\n\nprint(\"Preference data:\")\nprint(json.dumps([f.model_dump() for f in sdk.files.list(fileset=DATASET_NAME, workspace=\"default\").data], indent=2, default=str))", + "language": "python", + "source_html": "try:\n sdk.files.filesets.create(workspace="default", name=DATASET_NAME, description="DPO preference data")\n print(f"Created fileset: {DATASET_NAME}")\nexcept ConflictError:\n print(f"Fileset '{DATASET_NAME}' already exists, continuing...")\n\nsdk.files.upload(local_path=DATASET_PATH, remote_path="", fileset=DATASET_NAME, workspace="default")\n\nprint("Preference data:")\nprint(json.dumps([f.model_dump() for f in sdk.files.list(fileset=DATASET_NAME, workspace="default").data], indent=2, default=str))\n" + }, + { + "type": "markdown", + "source": "### 4. Secrets Setup\n\nThe base model (`meta-llama/Llama-3.2-1B-Instruct`) is gated, so store your Hugging Face token as a platform secret named `hf-token` and reference it on the model fileset.", + "source_html": "

4. Secrets Setup

\n

The base model (meta-llama/Llama-3.2-1B-Instruct) is gated, so store your Hugging Face token as a platform secret named hf-token and reference it on the model fileset.

\n" + }, + { + "type": "code", + "source": "HF_TOKEN = os.getenv(\"HF_TOKEN\")\nif not HF_TOKEN:\n raise RuntimeError(\"Set HF_TOKEN before running this tutorial.\")\n\ndef create_or_get_secret(name: str, value: str, label: str) -> PlatformSecretResponse:\n try:\n secret = sdk.secrets.create(name=name, workspace=\"default\", value=value)\n print(f\"Created secret: {name}\")\n return secret\n except ConflictError:\n print(f\"Secret '{name}' already exists, continuing...\")\n return sdk.secrets.retrieve(name=name, workspace=\"default\")\n\n\nhf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")", + "language": "python", + "source_html": "HF_TOKEN = os.getenv("HF_TOKEN")\nif not HF_TOKEN:\n raise RuntimeError("Set HF_TOKEN before running this tutorial.")\n\ndef create_or_get_secret(name: str, value: str, label: str) -> PlatformSecretResponse:\n try:\n secret = sdk.secrets.create(name=name, workspace="default", value=value)\n print(f"Created secret: {name}")\n return secret\n except ConflictError:\n print(f"Secret '{name}' already exists, continuing...")\n return sdk.secrets.retrieve(name=name, workspace="default")\n\n\nhf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN")\n" + }, + { + "type": "markdown", + "source": "### 5. Create Base Model FileSet and Model Entity\n\nDPO starts from an instruction-tuned base model. The model entity's spec is inferred asynchronously after creation.", + "source_html": "

5. Create Base Model FileSet and Model Entity

\n

DPO starts from an instruction-tuned base model. The model entity's spec is inferred asynchronously after creation.

\n" + }, + { + "type": "code", + "source": "HF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\nMODEL_NAME = \"llama-3-2-1b-instruct\"\n\nstorage = HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n token_secret=hf_secret.name,\n)\n\ntry:\n base_model_fs = sdk.files.filesets.create(\n workspace=\"default\", name=MODEL_NAME, description=\"Llama 3.2 1B Instruct base model\", storage=storage\n )\n print(f\"Created base model fileset: {MODEL_NAME}\")\nexcept ConflictError:\n base_model_fs = sdk.files.filesets.retrieve(workspace=\"default\", name=MODEL_NAME)\n print(\"Base model fileset already exists.\")\n\ntry:\n base_model = sdk.models.create(workspace=\"default\", name=MODEL_NAME, fileset=f\"default/{MODEL_NAME}\")\nexcept ConflictError:\n base_model = sdk.models.retrieve(workspace=\"default\", name=MODEL_NAME)\n\nprint(f\"Base model fileset: fileset://default/{base_model.name}\")\n\n# Wait for the ModelSpec to be inferred from the checkpoint.\ncheck = max_wait_time_checker(600, \"Model spec\")\nwhile not base_model.spec:\n check()\n time.sleep(10)\n base_model = sdk.models.retrieve(workspace=\"default\", name=MODEL_NAME)\nprint(\"Model spec ready\")", + "language": "python", + "source_html": "HF_REPO_ID = "meta-llama/Llama-3.2-1B-Instruct"\nMODEL_NAME = "llama-3-2-1b-instruct"\n\nstorage = HuggingfaceStorageConfigParam(\n type="huggingface",\n repo_id=HF_REPO_ID,\n repo_type="model",\n token_secret=hf_secret.name,\n)\n\ntry:\n base_model_fs = sdk.files.filesets.create(\n workspace="default", name=MODEL_NAME, description="Llama 3.2 1B Instruct base model", storage=storage\n )\n print(f"Created base model fileset: {MODEL_NAME}")\nexcept ConflictError:\n base_model_fs = sdk.files.filesets.retrieve(workspace="default", name=MODEL_NAME)\n print("Base model fileset already exists.")\n\ntry:\n base_model = sdk.models.create(workspace="default", name=MODEL_NAME, fileset=f"default/{MODEL_NAME}")\nexcept ConflictError:\n base_model = sdk.models.retrieve(workspace="default", name=MODEL_NAME)\n\nprint(f"Base model fileset: fileset://default/{base_model.name}")\n\n# Wait for the ModelSpec to be inferred from the checkpoint.\ncheck = max_wait_time_checker(600, "Model spec")\nwhile not base_model.spec:\n check()\n time.sleep(10)\n base_model = sdk.models.retrieve(workspace="default", name=MODEL_NAME)\nprint("Model spec ready")\n" + }, + { + "type": "markdown", + "source": "### 6. Create the DPO Customization Job\n\nSubmit a DPO job to the `rl` backend with `RlJobInput`. Note the DPO-specific shape:\n\n- `model` is a string ref to the model entity; `dataset` is a **single** string ref to the preference fileset (holding both files).\n- The training method is `{\"type\": \"dpo\", ...}` — full-weight, no `finetuning_type`/LoRA.\n- `ref_policy_kl_penalty` is **β** (DPO paper): how strongly the policy stays tied to the reference model.\n- `rl` auto-generates the job id (`rl-`); read it back from the response.\n\nOther configurable knobs: `optimizer_type`, `adam_eps`, `activation_checkpointing`, `keep_top_k`, `val_at_end`, `preference_loss_weight`, `sft_loss_weight`. Run `nemo customization rl explain` for the live schema.", + "source_html": "

6. Create the DPO Customization Job

\n

Submit a DPO job to the rl backend with RlJobInput. Note the DPO-specific shape:

\n
    \n
  • model is a string ref to the model entity; dataset is a single string ref to the preference fileset (holding both files).
  • \n
  • The training method is {"type": "dpo", ...} — full-weight, no finetuning_type/LoRA.
  • \n
  • ref_policy_kl_penalty is β (DPO paper): how strongly the policy stays tied to the reference model.
  • \n
  • rl auto-generates the job id (rl-<hex>); read it back from the response.
  • \n
\n

Other configurable knobs: optimizer_type, adam_eps, activation_checkpointing, keep_top_k, val_at_end, preference_loss_weight, sft_loss_weight. Run nemo customization rl explain for the live schema.

\n" + }, + { + "type": "code", + "source": "job_suffix = uuid.uuid4().hex[:8]\nOUTPUT_NAME = f\"llama-3-2-1b-dpo-{job_suffix}\"\n\nspec = RlJobInput(\n model=f\"default/{base_model.name}\",\n dataset=f\"default/{DATASET_NAME}\",\n training={\n \"type\": \"dpo\",\n \"epochs\": 1,\n \"batch_size\": 16,\n \"micro_batch_size\": 1,\n \"learning_rate\": 5e-6,\n \"max_seq_length\": 4096,\n \"ref_policy_kl_penalty\": 0.1,\n \"parallelism\": {\n \"num_nodes\": 1,\n \"num_gpus_per_node\": 1,\n \"tensor_parallel_size\": 1,\n \"pipeline_parallel_size\": 1,\n },\n },\n output={\"name\": OUTPUT_NAME},\n)\n\n# `rl` auto-generates the job id (rl-); do not pass name=.\njob = sdk.customization.rl.jobs.create(spec=spec, workspace=\"default\")\nprint(f\"Job ID: {job.job.name}\")\nprint(f\"Output model: {OUTPUT_NAME}\")", + "language": "python", + "source_html": "job_suffix = uuid.uuid4().hex[:8]\nOUTPUT_NAME = f"llama-3-2-1b-dpo-{job_suffix}"\n\nspec = RlJobInput(\n model=f"default/{base_model.name}",\n dataset=f"default/{DATASET_NAME}",\n training={\n "type": "dpo",\n "epochs": 1,\n "batch_size": 16,\n "micro_batch_size": 1,\n "learning_rate": 5e-6,\n "max_seq_length": 4096,\n "ref_policy_kl_penalty": 0.1,\n "parallelism": {\n "num_nodes": 1,\n "num_gpus_per_node": 1,\n "tensor_parallel_size": 1,\n "pipeline_parallel_size": 1,\n },\n },\n output={"name": OUTPUT_NAME},\n)\n\n# `rl` auto-generates the job id (rl-<hex>); do not pass name=.\njob = sdk.customization.rl.jobs.create(spec=spec, workspace="default")\nprint(f"Job ID: {job.job.name}")\nprint(f"Output model: {OUTPUT_NAME}")\n" + }, + { + "type": "markdown", + "source": "### 7. Track Training Progress\n\nThe DPO job runs four steps: download -> **dpo-training** (Ray) -> upload -> model-entity. We poll the top-level job status and surface the training step's progress.", + "source_html": "

7. Track Training Progress

\n

The DPO job runs four steps: download -> dpo-training (Ray) -> upload -> model-entity. We poll the top-level job status and surface the training step's progress.

\n" + }, + { + "type": "code", + "source": "from IPython.display import clear_output\n\ncheck = max_wait_time_checker(7200, \"DPO job\")\nwhile True:\n check()\n status = sdk.jobs.get_status(name=job.job.name, workspace=\"default\")\n clear_output(wait=True)\n print(f\"Job Status: {status.status}\")\n\n step = max_steps = phase = None\n for job_step in status.steps or []:\n if job_step.name == \"dpo-training\":\n for task in job_step.tasks or []:\n d = task.status_details or {}\n step, max_steps, phase = d.get(\"step\"), d.get(\"max_steps\"), d.get(\"phase\")\n break\n break\n if step is not None and max_steps:\n print(f\"Training: Step {step}/{max_steps} ({100 * step / max_steps:.1f}%)\")\n if phase:\n print(f\"Phase: {phase}\")\n\n if status.status in (\"completed\", \"failed\", \"cancelled\", \"error\"):\n print(f\"\\nJob finished: {status.status}\")\n break\n time.sleep(15)\n\nassert status.status == \"completed\"", + "language": "python", + "source_html": "from IPython.display import clear_output\n\ncheck = max_wait_time_checker(7200, "DPO job")\nwhile True:\n check()\n status = sdk.jobs.get_status(name=job.job.name, workspace="default")\n clear_output(wait=True)\n print(f"Job Status: {status.status}")\n\n step = max_steps = phase = None\n for job_step in status.steps or []:\n if job_step.name == "dpo-training":\n for task in job_step.tasks or []:\n d = task.status_details or {}\n step, max_steps, phase = d.get("step"), d.get("max_steps"), d.get("phase")\n break\n break\n if step is not None and max_steps:\n print(f"Training: Step {step}/{max_steps} ({100 * step / max_steps:.1f}%)")\n if phase:\n print(f"Phase: {phase}")\n\n if status.status in ("completed", "failed", "cancelled", "error"):\n print(f"\\nJob finished: {status.status}")\n break\n time.sleep(15)\n\nassert status.status == "completed"\n" + }, + { + "type": "markdown", + "source": "**Interpreting DPO training metrics** (in `status_details.metrics`):\n\n- **`loss`** — the DPO loss; should trend down as the policy learns to separate chosen from rejected.\n- **Reward margin** (chosen minus rejected reward) — should trend **up**: the model increasingly prefers chosen responses.\n- **Validation `loss`** — watch for divergence from training loss (overfitting). Raise `ref_policy_kl_penalty` (β) or add `sft_loss_weight` if the policy drifts too far from the reference.", + "source_html": "

Interpreting DPO training metrics (in status_details.metrics):

\n
    \n
  • loss — the DPO loss; should trend down as the policy learns to separate chosen from rejected.
  • \n
  • Reward margin (chosen minus rejected reward) — should trend up: the model increasingly prefers chosen responses.
  • \n
  • Validation loss — watch for divergence from training loss (overfitting). Raise ref_policy_kl_penalty (β) or add sft_loss_weight if the policy drifts too far from the reference.
  • \n
\n" + }, + { + "type": "markdown", + "source": "### 8. Validate the Output Model\n\nDPO produces a **full-weight model entity** (not an adapter). Confirm it was registered.", + "source_html": "

8. Validate the Output Model

\n

DPO produces a full-weight model entity (not an adapter). Confirm it was registered.

\n" + }, + { + "type": "code", + "source": "model_entity = sdk.models.retrieve(workspace=\"default\", name=OUTPUT_NAME)\nprint(model_entity.model_dump_json(indent=2))", + "language": "python", + "source_html": "model_entity = sdk.models.retrieve(workspace="default", name=OUTPUT_NAME)\nprint(model_entity.model_dump_json(indent=2))\n" + }, + { + "type": "markdown", + "source": "### 9. Deploy and Evaluate (optional)\n\nThe DPO output is a full model, so it deploys like any full-weight checkpoint (see the [Full SFT](/documentation/customizer-reference/tutorials/sft-customization-job) tutorial for details). We deploy with vLLM and send a chat completion.", + "source_html": "

9. Deploy and Evaluate (optional)

\n

The DPO output is a full model, so it deploys like any full-weight checkpoint (see the Full SFT tutorial for details). We deploy with vLLM and send a chat completion.

\n" + }, + { + "type": "code", + "source": "deploy_suffix = uuid.uuid4().hex[:8]\nDEPLOYMENT_CONFIG_NAME = f\"dpo-deployment-cfg-{deploy_suffix}\"\nDEPLOYMENT_NAME = f\"dpo-deployment-{deploy_suffix}\"\n\ndeployment_config = sdk.inference.deployment_configs.create(\n workspace=\"default\",\n name=DEPLOYMENT_CONFIG_NAME,\n engine=\"vllm\",\n model_spec={\"model_namespace\": \"default\", \"model_name\": OUTPUT_NAME},\n executor_config={\"gpu\": 1, \"image_name\": \"vllm/vllm-openai\", \"image_tag\": \"v0.22.1\"},\n)\n\ndeployment = sdk.inference.deployments.create(\n workspace=\"default\", name=DEPLOYMENT_NAME, config=deployment_config.name\n)\nprint(f\"Deployment name: {deployment.name}\")", + "language": "python", + "source_html": "deploy_suffix = uuid.uuid4().hex[:8]\nDEPLOYMENT_CONFIG_NAME = f"dpo-deployment-cfg-{deploy_suffix}"\nDEPLOYMENT_NAME = f"dpo-deployment-{deploy_suffix}"\n\ndeployment_config = sdk.inference.deployment_configs.create(\n workspace="default",\n name=DEPLOYMENT_CONFIG_NAME,\n engine="vllm",\n model_spec={"model_namespace": "default", "model_name": OUTPUT_NAME},\n executor_config={"gpu": 1, "image_name": "vllm/vllm-openai", "image_tag": "v0.22.1"},\n)\n\ndeployment = sdk.inference.deployments.create(\n workspace="default", name=DEPLOYMENT_NAME, config=deployment_config.name\n)\nprint(f"Deployment name: {deployment.name}")\n" + }, + { + "type": "code", + "source": "check = max_wait_time_checker(1800, \"Deployment\")\nwhile True:\n check()\n deployment_status = sdk.inference.deployments.retrieve(name=deployment.name, workspace=\"default\")\n clear_output(wait=True)\n print(f\"Deployment status: {deployment_status.status}\")\n deployment_state = str(deployment_status.status).lower()\n if deployment_state in (\"ready\", \"running\"):\n if not sdk.models.wait_for_gateway(deployment.name, workspace=\"default\", timeout=60):\n raise RuntimeError(\"Inference gateway did not become ready\")\n break\n if deployment_state in (\"failed\", \"error\", \"terminated\", \"lost\"):\n raise RuntimeError(f\"Deployment failed with status: {deployment_status.status}\")\n time.sleep(15)", + "language": "python", + "source_html": "check = max_wait_time_checker(1800, "Deployment")\nwhile True:\n check()\n deployment_status = sdk.inference.deployments.retrieve(name=deployment.name, workspace="default")\n clear_output(wait=True)\n print(f"Deployment status: {deployment_status.status}")\n deployment_state = str(deployment_status.status).lower()\n if deployment_state in ("ready", "running"):\n if not sdk.models.wait_for_gateway(deployment.name, workspace="default", timeout=60):\n raise RuntimeError("Inference gateway did not become ready")\n break\n if deployment_state in ("failed", "error", "terminated", "lost"):\n raise RuntimeError(f"Deployment failed with status: {deployment_status.status}")\n time.sleep(15)\n" + }, + { + "type": "code", + "source": "messages = [\n {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n {\"role\": \"user\", \"content\": \"Write a short, friendly email to a colleague asking to reschedule our meeting to Thursday.\"},\n]\n\nresponse = sdk.inference.gateway.provider.post(\n \"v1/chat/completions\",\n name=deployment.name,\n workspace=\"default\",\n body={\"model\": f\"default/{OUTPUT_NAME}\", \"messages\": messages, \"temperature\": 0.7, \"max_tokens\": 256},\n)\nprint(\"Model output:\\n\")\nprint(response[\"choices\"][0][\"message\"][\"content\"])", + "language": "python", + "source_html": "messages = [\n {"role": "system", "content": "You are a helpful assistant."},\n {"role": "user", "content": "Write a short, friendly email to a colleague asking to reschedule our meeting to Thursday."},\n]\n\nresponse = sdk.inference.gateway.provider.post(\n "v1/chat/completions",\n name=deployment.name,\n workspace="default",\n body={"model": f"default/{OUTPUT_NAME}", "messages": messages, "temperature": 0.7, "max_tokens": 256},\n)\nprint("Model output:\\n")\nprint(response["choices"][0]["message"]["content"])\n" + }, + { + "type": "markdown", + "source": "## Conclusion\n\nYou aligned a base model with **DPO** on the NeMo Platform using the `rl` backend:\n\n- Uploaded a HelpSteer3 preference dataset **as-is** (the platform detects the schema natively).\n- Submitted a full-weight DPO job that ran on a Ray cluster via the Kubernetes executor.\n- Registered the output as a full model entity and (optionally) deployed it for inference.\n\n**Next steps:** tune the alignment strength with `ref_policy_kl_penalty` (β), add `sft_loss_weight` to anchor the policy to the chosen responses, enable `activation_checkpointing` for memory headroom, or scale up with `parallelism`. See the [Training Configuration](/documentation/customizer-reference/manage-customization-jobs/training-configuration) reference for the full hyperparameter set.", + "source_html": "

Conclusion

\n

You aligned a base model with DPO on the NeMo Platform using the rl backend:

\n
    \n
  • Uploaded a HelpSteer3 preference dataset as-is (the platform detects the schema natively).
  • \n
  • Submitted a full-weight DPO job that ran on a Ray cluster via the Kubernetes executor.
  • \n
  • Registered the output as a full model entity and (optionally) deployed it for inference.
  • \n
\n

Next steps: tune the alignment strength with ref_policy_kl_penalty (β), add sft_loss_weight to anchor the policy to the chosen responses, enable activation_checkpointing for memory headroom, or scale up with parallelism. See the Training Configuration reference for the full hyperparameter set.

\n" + } + ] +} \ No newline at end of file diff --git a/docs/fern/components/notebooks/dpo-customization-job.ts b/docs/fern/components/notebooks/dpo-customization-job.ts new file mode 100644 index 0000000000..fc8e558903 --- /dev/null +++ b/docs/fern/components/notebooks/dpo-customization-job.ts @@ -0,0 +1,159 @@ +/** + * SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Auto-generated by ipynb-to-fern-json.py - do not edit manually. + */ +export default { cells: [ + { + "type": "markdown", + "source": "\n\n\n# DPO Model Customization Job\n\nLearn how to use the NeMo Platform to align a model with **DPO** (Direct Preference Optimization) on a preference dataset. For each prompt, DPO trains on a *chosen* (preferred) and a *rejected* response so the model prefers the chosen style — no separate reward model required.\n\nThis tutorial uses the `rl` customization backend (powered by [NVIDIA NeMo-RL](https://github.com/NVIDIA-NeMo/RL)), which runs DPO on a **Ray** cluster. Unlike the [SFT](/documentation/customizer-reference/tutorials/sft-customization-job) and [LoRA](/documentation/customizer-reference/tutorials/lora-customization-job) tutorials (Docker GPU jobs), `rl` requires a **Kubernetes-backed** NeMo Platform. DPO here is **full-weight** (no LoRA/adapter); the output is a full model entity.\n\n**Time to complete:** approximately 45-60 minutes. Job duration increases with model and dataset size.", + "source_html": "\n\n

DPO Model Customization Job

\n

Learn how to use the NeMo Platform to align a model with DPO (Direct Preference Optimization) on a preference dataset. For each prompt, DPO trains on a chosen (preferred) and a rejected response so the model prefers the chosen style — no separate reward model required.

\n

This tutorial uses the rl customization backend (powered by NVIDIA NeMo-RL), which runs DPO on a Ray cluster. Unlike the SFT and LoRA tutorials (Docker GPU jobs), rl requires a Kubernetes-backed NeMo Platform. DPO here is full-weight (no LoRA/adapter); the output is a full model entity.

\n

Time to complete: approximately 45-60 minutes. Job duration increases with model and dataset size.

\n" + }, + { + "type": "markdown", + "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](/documentation/get-started)** to install the NeMo Platform and Python SDK.\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root).\n3. **Installed the `datasets` package**: `pip install datasets`.\n4. **A platform configured with `platform.runtime: kubernetes`.** The `rl` (DPO) backend provisions a Ray cluster and has **no local Docker fallback** — `submit` fails fast on a Docker-runtime platform. Multi-node jobs (`parallelism.num_nodes > 1`) additionally require the platform-side `NMP_RL_MULTINODE_SHARED_STORAGE_PATH`.\n5. **A Hugging Face token** with access to the gated base model (this tutorial uses `meta-llama/Llama-3.2-1B-Instruct`). Export it as `HF_TOKEN`.\n6. **At least one GPU with CUDA 13+** and a GPU execution profile (`nemo jobs list-execution-profiles`).", + "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install the NeMo Platform and Python SDK.
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root).
  4. \n
  5. Installed the datasets package: pip install datasets.
  6. \n
  7. A platform configured with platform.runtime: kubernetes. The rl (DPO) backend provisions a Ray cluster and has no local Docker fallbacksubmit fails fast on a Docker-runtime platform. Multi-node jobs (parallelism.num_nodes > 1) additionally require the platform-side NMP_RL_MULTINODE_SHARED_STORAGE_PATH.
  8. \n
  9. A Hugging Face token with access to the gated base model (this tutorial uses meta-llama/Llama-3.2-1B-Instruct). Export it as HF_TOKEN.
  10. \n
  11. At least one GPU with CUDA 13+ and a GPU execution profile (nemo jobs list-execution-profiles).
  12. \n
\n" + }, + { + "type": "markdown", + "source": "## Quick Start\n\n### 1. Initialize the SDK\n\nThe SDK needs your NeMo Platform server URL. By default `http://localhost:8080` is used; set `NMP_BASE_URL` to override:\n\n```sh\nexport NMP_BASE_URL=\n```", + "source_html": "

Quick Start

\n

1. Initialize the SDK

\n

The SDK needs your NeMo Platform server URL. By default http://localhost:8080 is used; set NMP_BASE_URL to override:

\n
export NMP_BASE_URL=<YOUR_NMP_BASE_URL>\n
\n" + }, + { + "type": "code", + "source": "import json\nimport os\nimport time\nimport uuid\nfrom pathlib import Path\nfrom nemo_platform import NeMoPlatform, ConflictError\nfrom nemo_platform.types.secrets import PlatformSecretResponse\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\nfrom nemo_rl_plugin.schema import RlJobInput\n\n\ndef max_wait_time_checker(seconds: int, label: str = \"\"):\n \"\"\"Return a check() that raises TimeoutError once `seconds` have elapsed.\"\"\"\n start = time.time()\n\n def check():\n if time.time() - start > seconds:\n raise TimeoutError(f\"{label} took longer than {seconds} seconds\")\n\n return check\n\n\nNMP_BASE_URL = os.environ.get(\"NMP_BASE_URL\", \"http://localhost:8080\")\nsdk = NeMoPlatform(base_url=NMP_BASE_URL, workspace=\"default\")", + "language": "python", + "source_html": "import json\nimport os\nimport time\nimport uuid\nfrom pathlib import Path\nfrom nemo_platform import NeMoPlatform, ConflictError\nfrom nemo_platform.types.secrets import PlatformSecretResponse\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\nfrom nemo_rl_plugin.schema import RlJobInput\n\n\ndef max_wait_time_checker(seconds: int, label: str = ""):\n """Return a check() that raises TimeoutError once `seconds` have elapsed."""\n start = time.time()\n\n def check():\n if time.time() - start > seconds:\n raise TimeoutError(f"{label} took longer than {seconds} seconds")\n\n return check\n\n\nNMP_BASE_URL = os.environ.get("NMP_BASE_URL", "http://localhost:8080")\nsdk = NeMoPlatform(base_url=NMP_BASE_URL, workspace="default")\n" + }, + { + "type": "markdown", + "source": "### 2. Prepare the Preference Dataset\n\nDPO trains on **preference data**. The `rl` backend takes a **single** dataset fileset that holds both `training.jsonl` and `validation.jsonl`, and auto-detects the row schema from the first line. Three preference formats are supported (see the platform's `BinaryPreferenceDatasetItemSchema` / `HelpSteer3DatasetItemSchema` / `Tulu3PreferenceDatasetItemSchema`):", + "source_html": "

2. Prepare the Preference Dataset

\n

DPO trains on preference data. The rl backend takes a single dataset fileset that holds both training.jsonl and validation.jsonl, and auto-detects the row schema from the first line. Three preference formats are supported (see the platform's BinaryPreferenceDatasetItemSchema / HelpSteer3DatasetItemSchema / Tulu3PreferenceDatasetItemSchema):

\n" + }, + { + "type": "markdown", + "source": "#### Binary Preference Format\n\nSimple `prompt` / `chosen` / `rejected` (the `prompt` may be a string or a list of chat messages):\n\n```json\n{\"prompt\": \"What is the capital of France?\", \"chosen\": \"The capital of France is Paris.\", \"rejected\": \"I'm not sure.\"}\n```", + "source_html": "

Binary Preference Format

\n

Simple prompt / chosen / rejected (the prompt may be a string or a list of chat messages):

\n
{"prompt": "What is the capital of France?", "chosen": "The capital of France is Paris.", "rejected": "I'm not sure."}\n
\n" + }, + { + "type": "markdown", + "source": "#### HelpSteer3 Format (used here)\n\nA conversation `context` (string or chat messages), two candidate `response1` / `response2`, and a signed `overall_preference` in -3..3 — **negative** means response 1 is preferred, **positive** means response 2, **0** is a tie. This is the **raw** schema of `nvidia/HelpSteer3`, so no conversion is needed:\n\n```json\n{\"context\": [{\"role\": \"user\", \"content\": \"Explain how to use git rebase\"}], \"response1\": \"...\", \"response2\": \"...\", \"overall_preference\": -2}\n```", + "source_html": "

HelpSteer3 Format (used here)

\n

A conversation context (string or chat messages), two candidate response1 / response2, and a signed overall_preference in -3..3 — negative means response 1 is preferred, positive means response 2, 0 is a tie. This is the raw schema of nvidia/HelpSteer3, so no conversion is needed:

\n
{"context": [{"role": "user", "content": "Explain how to use git rebase"}], "response1": "...", "response2": "...", "overall_preference": -2}\n
\n" + }, + { + "type": "markdown", + "source": "#### Tulu3 Preference Format\n\nFull chat conversations for both the chosen and rejected branches (each a list of messages ending with the assistant turn):\n\n```json\n{\"chosen\": [{\"role\": \"user\", \"content\": \"...\"}, {\"role\": \"assistant\", \"content\": \"preferred\"}], \"rejected\": [{\"role\": \"user\", \"content\": \"...\"}, {\"role\": \"assistant\", \"content\": \"dispreferred\"}]}\n```", + "source_html": "

Tulu3 Preference Format

\n

Full chat conversations for both the chosen and rejected branches (each a list of messages ending with the assistant turn):

\n
{"chosen": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "preferred"}], "rejected": [{"role": "user", "content": "..."}, {"role": "assistant", "content": "dispreferred"}]}\n
\n" + }, + { + "type": "markdown", + "source": "#### Download nvidia/HelpSteer3\n\nWe use [nvidia/HelpSteer3](https://huggingface.co/datasets/nvidia/HelpSteer3) (the `preference` subset), NVIDIA's open preference dataset. It ships native `train` and `validation` splits and matches the HelpSteer3 schema above, so we upload the rows **as-is** — the platform's `HelpSteer3Dataset` loader handles the `overall_preference` semantics (including ties) at training time.", + "source_html": "

Download nvidia/HelpSteer3

\n

We use nvidia/HelpSteer3 (the preference subset), NVIDIA's open preference dataset. It ships native train and validation splits and matches the HelpSteer3 schema above, so we upload the rows as-is — the platform's HelpSteer3Dataset loader handles the overall_preference semantics (including ties) at training time.

\n" + }, + { + "type": "code", + "source": "from datasets import load_dataset, Dataset\n\nprint(\"Loading dataset nvidia/HelpSteer3 (preference subset)\")\nds = load_dataset(\"nvidia/HelpSteer3\", \"preference\")\n\n# Small subsets keep the tutorial fast; larger sets train better but take longer.\ntraining_size = 3000\nvalidation_size = 300\nDATASET_NAME = \"dpo-dataset\"\nDATASET_PATH = Path(\"dpo-dataset\").absolute()\nos.makedirs(DATASET_PATH, exist_ok=True)\n\ntrain_dataset = ds[\"train\"]\nvalidation_dataset = ds[\"validation\"]\nassert isinstance(train_dataset, Dataset) and isinstance(validation_dataset, Dataset)\n\n# Save raw HelpSteer3 rows directly — no conversion. The platform detects the\n# HelpSteer3 schema from the row keys (context / response1 / response2 / overall_preference).\ntrain_dataset.select(range(training_size)).to_json(f\"{DATASET_PATH}/training.jsonl\")\nvalidation_dataset.select(range(validation_size)).to_json(f\"{DATASET_PATH}/validation.jsonl\")\n\nprint(f\"Saved training.jsonl ({training_size} rows) and validation.jsonl ({validation_size} rows)\")\nwith open(f\"{DATASET_PATH}/training.jsonl\") as f:\n sample = json.loads(f.readline())\nprint(\"Sample keys:\", sorted(sample.keys()))\nprint(\"overall_preference:\", sample[\"overall_preference\"])", + "language": "python", + "source_html": "from datasets import load_dataset, Dataset\n\nprint("Loading dataset nvidia/HelpSteer3 (preference subset)")\nds = load_dataset("nvidia/HelpSteer3", "preference")\n\n# Small subsets keep the tutorial fast; larger sets train better but take longer.\ntraining_size = 3000\nvalidation_size = 300\nDATASET_NAME = "dpo-dataset"\nDATASET_PATH = Path("dpo-dataset").absolute()\nos.makedirs(DATASET_PATH, exist_ok=True)\n\ntrain_dataset = ds["train"]\nvalidation_dataset = ds["validation"]\nassert isinstance(train_dataset, Dataset) and isinstance(validation_dataset, Dataset)\n\n# Save raw HelpSteer3 rows directly — no conversion. The platform detects the\n# HelpSteer3 schema from the row keys (context / response1 / response2 / overall_preference).\ntrain_dataset.select(range(training_size)).to_json(f"{DATASET_PATH}/training.jsonl")\nvalidation_dataset.select(range(validation_size)).to_json(f"{DATASET_PATH}/validation.jsonl")\n\nprint(f"Saved training.jsonl ({training_size} rows) and validation.jsonl ({validation_size} rows)")\nwith open(f"{DATASET_PATH}/training.jsonl") as f:\n sample = json.loads(f.readline())\nprint("Sample keys:", sorted(sample.keys()))\nprint("overall_preference:", sample["overall_preference"])\n" + }, + { + "type": "markdown", + "source": "### 3. Create FileSet and Upload Preference Data\n\nUpload both JSONL files to a single FileSet so the DPO job can read them.", + "source_html": "

3. Create FileSet and Upload Preference Data

\n

Upload both JSONL files to a single FileSet so the DPO job can read them.

\n" + }, + { + "type": "code", + "source": "try:\n sdk.files.filesets.create(workspace=\"default\", name=DATASET_NAME, description=\"DPO preference data\")\n print(f\"Created fileset: {DATASET_NAME}\")\nexcept ConflictError:\n print(f\"Fileset '{DATASET_NAME}' already exists, continuing...\")\n\nsdk.files.upload(local_path=DATASET_PATH, remote_path=\"\", fileset=DATASET_NAME, workspace=\"default\")\n\nprint(\"Preference data:\")\nprint(json.dumps([f.model_dump() for f in sdk.files.list(fileset=DATASET_NAME, workspace=\"default\").data], indent=2, default=str))", + "language": "python", + "source_html": "try:\n sdk.files.filesets.create(workspace="default", name=DATASET_NAME, description="DPO preference data")\n print(f"Created fileset: {DATASET_NAME}")\nexcept ConflictError:\n print(f"Fileset '{DATASET_NAME}' already exists, continuing...")\n\nsdk.files.upload(local_path=DATASET_PATH, remote_path="", fileset=DATASET_NAME, workspace="default")\n\nprint("Preference data:")\nprint(json.dumps([f.model_dump() for f in sdk.files.list(fileset=DATASET_NAME, workspace="default").data], indent=2, default=str))\n" + }, + { + "type": "markdown", + "source": "### 4. Secrets Setup\n\nThe base model (`meta-llama/Llama-3.2-1B-Instruct`) is gated, so store your Hugging Face token as a platform secret named `hf-token` and reference it on the model fileset.", + "source_html": "

4. Secrets Setup

\n

The base model (meta-llama/Llama-3.2-1B-Instruct) is gated, so store your Hugging Face token as a platform secret named hf-token and reference it on the model fileset.

\n" + }, + { + "type": "code", + "source": "HF_TOKEN = os.getenv(\"HF_TOKEN\")\nif not HF_TOKEN:\n raise RuntimeError(\"Set HF_TOKEN before running this tutorial.\")\n\ndef create_or_get_secret(name: str, value: str, label: str) -> PlatformSecretResponse:\n try:\n secret = sdk.secrets.create(name=name, workspace=\"default\", value=value)\n print(f\"Created secret: {name}\")\n return secret\n except ConflictError:\n print(f\"Secret '{name}' already exists, continuing...\")\n return sdk.secrets.retrieve(name=name, workspace=\"default\")\n\n\nhf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")", + "language": "python", + "source_html": "HF_TOKEN = os.getenv("HF_TOKEN")\nif not HF_TOKEN:\n raise RuntimeError("Set HF_TOKEN before running this tutorial.")\n\ndef create_or_get_secret(name: str, value: str, label: str) -> PlatformSecretResponse:\n try:\n secret = sdk.secrets.create(name=name, workspace="default", value=value)\n print(f"Created secret: {name}")\n return secret\n except ConflictError:\n print(f"Secret '{name}' already exists, continuing...")\n return sdk.secrets.retrieve(name=name, workspace="default")\n\n\nhf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN")\n" + }, + { + "type": "markdown", + "source": "### 5. Create Base Model FileSet and Model Entity\n\nDPO starts from an instruction-tuned base model. The model entity's spec is inferred asynchronously after creation.", + "source_html": "

5. Create Base Model FileSet and Model Entity

\n

DPO starts from an instruction-tuned base model. The model entity's spec is inferred asynchronously after creation.

\n" + }, + { + "type": "code", + "source": "HF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\nMODEL_NAME = \"llama-3-2-1b-instruct\"\n\nstorage = HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n token_secret=hf_secret.name,\n)\n\ntry:\n base_model_fs = sdk.files.filesets.create(\n workspace=\"default\", name=MODEL_NAME, description=\"Llama 3.2 1B Instruct base model\", storage=storage\n )\n print(f\"Created base model fileset: {MODEL_NAME}\")\nexcept ConflictError:\n base_model_fs = sdk.files.filesets.retrieve(workspace=\"default\", name=MODEL_NAME)\n print(\"Base model fileset already exists.\")\n\ntry:\n base_model = sdk.models.create(workspace=\"default\", name=MODEL_NAME, fileset=f\"default/{MODEL_NAME}\")\nexcept ConflictError:\n base_model = sdk.models.retrieve(workspace=\"default\", name=MODEL_NAME)\n\nprint(f\"Base model fileset: fileset://default/{base_model.name}\")\n\n# Wait for the ModelSpec to be inferred from the checkpoint.\ncheck = max_wait_time_checker(600, \"Model spec\")\nwhile not base_model.spec:\n check()\n time.sleep(10)\n base_model = sdk.models.retrieve(workspace=\"default\", name=MODEL_NAME)\nprint(\"Model spec ready\")", + "language": "python", + "source_html": "HF_REPO_ID = "meta-llama/Llama-3.2-1B-Instruct"\nMODEL_NAME = "llama-3-2-1b-instruct"\n\nstorage = HuggingfaceStorageConfigParam(\n type="huggingface",\n repo_id=HF_REPO_ID,\n repo_type="model",\n token_secret=hf_secret.name,\n)\n\ntry:\n base_model_fs = sdk.files.filesets.create(\n workspace="default", name=MODEL_NAME, description="Llama 3.2 1B Instruct base model", storage=storage\n )\n print(f"Created base model fileset: {MODEL_NAME}")\nexcept ConflictError:\n base_model_fs = sdk.files.filesets.retrieve(workspace="default", name=MODEL_NAME)\n print("Base model fileset already exists.")\n\ntry:\n base_model = sdk.models.create(workspace="default", name=MODEL_NAME, fileset=f"default/{MODEL_NAME}")\nexcept ConflictError:\n base_model = sdk.models.retrieve(workspace="default", name=MODEL_NAME)\n\nprint(f"Base model fileset: fileset://default/{base_model.name}")\n\n# Wait for the ModelSpec to be inferred from the checkpoint.\ncheck = max_wait_time_checker(600, "Model spec")\nwhile not base_model.spec:\n check()\n time.sleep(10)\n base_model = sdk.models.retrieve(workspace="default", name=MODEL_NAME)\nprint("Model spec ready")\n" + }, + { + "type": "markdown", + "source": "### 6. Create the DPO Customization Job\n\nSubmit a DPO job to the `rl` backend with `RlJobInput`. Note the DPO-specific shape:\n\n- `model` is a string ref to the model entity; `dataset` is a **single** string ref to the preference fileset (holding both files).\n- The training method is `{\"type\": \"dpo\", ...}` — full-weight, no `finetuning_type`/LoRA.\n- `ref_policy_kl_penalty` is **β** (DPO paper): how strongly the policy stays tied to the reference model.\n- `rl` auto-generates the job id (`rl-`); read it back from the response.\n\nOther configurable knobs: `optimizer_type`, `adam_eps`, `activation_checkpointing`, `keep_top_k`, `val_at_end`, `preference_loss_weight`, `sft_loss_weight`. Run `nemo customization rl explain` for the live schema.", + "source_html": "

6. Create the DPO Customization Job

\n

Submit a DPO job to the rl backend with RlJobInput. Note the DPO-specific shape:

\n
    \n
  • model is a string ref to the model entity; dataset is a single string ref to the preference fileset (holding both files).
  • \n
  • The training method is {"type": "dpo", ...} — full-weight, no finetuning_type/LoRA.
  • \n
  • ref_policy_kl_penalty is β (DPO paper): how strongly the policy stays tied to the reference model.
  • \n
  • rl auto-generates the job id (rl-<hex>); read it back from the response.
  • \n
\n

Other configurable knobs: optimizer_type, adam_eps, activation_checkpointing, keep_top_k, val_at_end, preference_loss_weight, sft_loss_weight. Run nemo customization rl explain for the live schema.

\n" + }, + { + "type": "code", + "source": "job_suffix = uuid.uuid4().hex[:8]\nOUTPUT_NAME = f\"llama-3-2-1b-dpo-{job_suffix}\"\n\nspec = RlJobInput(\n model=f\"default/{base_model.name}\",\n dataset=f\"default/{DATASET_NAME}\",\n training={\n \"type\": \"dpo\",\n \"epochs\": 1,\n \"batch_size\": 16,\n \"micro_batch_size\": 1,\n \"learning_rate\": 5e-6,\n \"max_seq_length\": 4096,\n \"ref_policy_kl_penalty\": 0.1,\n \"parallelism\": {\n \"num_nodes\": 1,\n \"num_gpus_per_node\": 1,\n \"tensor_parallel_size\": 1,\n \"pipeline_parallel_size\": 1,\n },\n },\n output={\"name\": OUTPUT_NAME},\n)\n\n# `rl` auto-generates the job id (rl-); do not pass name=.\njob = sdk.customization.rl.jobs.create(spec=spec, workspace=\"default\")\nprint(f\"Job ID: {job.job.name}\")\nprint(f\"Output model: {OUTPUT_NAME}\")", + "language": "python", + "source_html": "job_suffix = uuid.uuid4().hex[:8]\nOUTPUT_NAME = f"llama-3-2-1b-dpo-{job_suffix}"\n\nspec = RlJobInput(\n model=f"default/{base_model.name}",\n dataset=f"default/{DATASET_NAME}",\n training={\n "type": "dpo",\n "epochs": 1,\n "batch_size": 16,\n "micro_batch_size": 1,\n "learning_rate": 5e-6,\n "max_seq_length": 4096,\n "ref_policy_kl_penalty": 0.1,\n "parallelism": {\n "num_nodes": 1,\n "num_gpus_per_node": 1,\n "tensor_parallel_size": 1,\n "pipeline_parallel_size": 1,\n },\n },\n output={"name": OUTPUT_NAME},\n)\n\n# `rl` auto-generates the job id (rl-<hex>); do not pass name=.\njob = sdk.customization.rl.jobs.create(spec=spec, workspace="default")\nprint(f"Job ID: {job.job.name}")\nprint(f"Output model: {OUTPUT_NAME}")\n" + }, + { + "type": "markdown", + "source": "### 7. Track Training Progress\n\nThe DPO job runs four steps: download -> **dpo-training** (Ray) -> upload -> model-entity. We poll the top-level job status and surface the training step's progress.", + "source_html": "

7. Track Training Progress

\n

The DPO job runs four steps: download -> dpo-training (Ray) -> upload -> model-entity. We poll the top-level job status and surface the training step's progress.

\n" + }, + { + "type": "code", + "source": "from IPython.display import clear_output\n\ncheck = max_wait_time_checker(7200, \"DPO job\")\nwhile True:\n check()\n status = sdk.jobs.get_status(name=job.job.name, workspace=\"default\")\n clear_output(wait=True)\n print(f\"Job Status: {status.status}\")\n\n step = max_steps = phase = None\n for job_step in status.steps or []:\n if job_step.name == \"dpo-training\":\n for task in job_step.tasks or []:\n d = task.status_details or {}\n step, max_steps, phase = d.get(\"step\"), d.get(\"max_steps\"), d.get(\"phase\")\n break\n break\n if step is not None and max_steps:\n print(f\"Training: Step {step}/{max_steps} ({100 * step / max_steps:.1f}%)\")\n if phase:\n print(f\"Phase: {phase}\")\n\n if status.status in (\"completed\", \"failed\", \"cancelled\", \"error\"):\n print(f\"\\nJob finished: {status.status}\")\n break\n time.sleep(15)\n\nassert status.status == \"completed\"", + "language": "python", + "source_html": "from IPython.display import clear_output\n\ncheck = max_wait_time_checker(7200, "DPO job")\nwhile True:\n check()\n status = sdk.jobs.get_status(name=job.job.name, workspace="default")\n clear_output(wait=True)\n print(f"Job Status: {status.status}")\n\n step = max_steps = phase = None\n for job_step in status.steps or []:\n if job_step.name == "dpo-training":\n for task in job_step.tasks or []:\n d = task.status_details or {}\n step, max_steps, phase = d.get("step"), d.get("max_steps"), d.get("phase")\n break\n break\n if step is not None and max_steps:\n print(f"Training: Step {step}/{max_steps} ({100 * step / max_steps:.1f}%)")\n if phase:\n print(f"Phase: {phase}")\n\n if status.status in ("completed", "failed", "cancelled", "error"):\n print(f"\\nJob finished: {status.status}")\n break\n time.sleep(15)\n\nassert status.status == "completed"\n" + }, + { + "type": "markdown", + "source": "**Interpreting DPO training metrics** (in `status_details.metrics`):\n\n- **`loss`** — the DPO loss; should trend down as the policy learns to separate chosen from rejected.\n- **Reward margin** (chosen minus rejected reward) — should trend **up**: the model increasingly prefers chosen responses.\n- **Validation `loss`** — watch for divergence from training loss (overfitting). Raise `ref_policy_kl_penalty` (β) or add `sft_loss_weight` if the policy drifts too far from the reference.", + "source_html": "

Interpreting DPO training metrics (in status_details.metrics):

\n
    \n
  • loss — the DPO loss; should trend down as the policy learns to separate chosen from rejected.
  • \n
  • Reward margin (chosen minus rejected reward) — should trend up: the model increasingly prefers chosen responses.
  • \n
  • Validation loss — watch for divergence from training loss (overfitting). Raise ref_policy_kl_penalty (β) or add sft_loss_weight if the policy drifts too far from the reference.
  • \n
\n" + }, + { + "type": "markdown", + "source": "### 8. Validate the Output Model\n\nDPO produces a **full-weight model entity** (not an adapter). Confirm it was registered.", + "source_html": "

8. Validate the Output Model

\n

DPO produces a full-weight model entity (not an adapter). Confirm it was registered.

\n" + }, + { + "type": "code", + "source": "model_entity = sdk.models.retrieve(workspace=\"default\", name=OUTPUT_NAME)\nprint(model_entity.model_dump_json(indent=2))", + "language": "python", + "source_html": "model_entity = sdk.models.retrieve(workspace="default", name=OUTPUT_NAME)\nprint(model_entity.model_dump_json(indent=2))\n" + }, + { + "type": "markdown", + "source": "### 9. Deploy and Evaluate (optional)\n\nThe DPO output is a full model, so it deploys like any full-weight checkpoint (see the [Full SFT](/documentation/customizer-reference/tutorials/sft-customization-job) tutorial for details). We deploy with vLLM and send a chat completion.", + "source_html": "

9. Deploy and Evaluate (optional)

\n

The DPO output is a full model, so it deploys like any full-weight checkpoint (see the Full SFT tutorial for details). We deploy with vLLM and send a chat completion.

\n" + }, + { + "type": "code", + "source": "deploy_suffix = uuid.uuid4().hex[:8]\nDEPLOYMENT_CONFIG_NAME = f\"dpo-deployment-cfg-{deploy_suffix}\"\nDEPLOYMENT_NAME = f\"dpo-deployment-{deploy_suffix}\"\n\ndeployment_config = sdk.inference.deployment_configs.create(\n workspace=\"default\",\n name=DEPLOYMENT_CONFIG_NAME,\n engine=\"vllm\",\n model_spec={\"model_namespace\": \"default\", \"model_name\": OUTPUT_NAME},\n executor_config={\"gpu\": 1, \"image_name\": \"vllm/vllm-openai\", \"image_tag\": \"v0.22.1\"},\n)\n\ndeployment = sdk.inference.deployments.create(\n workspace=\"default\", name=DEPLOYMENT_NAME, config=deployment_config.name\n)\nprint(f\"Deployment name: {deployment.name}\")", + "language": "python", + "source_html": "deploy_suffix = uuid.uuid4().hex[:8]\nDEPLOYMENT_CONFIG_NAME = f"dpo-deployment-cfg-{deploy_suffix}"\nDEPLOYMENT_NAME = f"dpo-deployment-{deploy_suffix}"\n\ndeployment_config = sdk.inference.deployment_configs.create(\n workspace="default",\n name=DEPLOYMENT_CONFIG_NAME,\n engine="vllm",\n model_spec={"model_namespace": "default", "model_name": OUTPUT_NAME},\n executor_config={"gpu": 1, "image_name": "vllm/vllm-openai", "image_tag": "v0.22.1"},\n)\n\ndeployment = sdk.inference.deployments.create(\n workspace="default", name=DEPLOYMENT_NAME, config=deployment_config.name\n)\nprint(f"Deployment name: {deployment.name}")\n" + }, + { + "type": "code", + "source": "check = max_wait_time_checker(1800, \"Deployment\")\nwhile True:\n check()\n deployment_status = sdk.inference.deployments.retrieve(name=deployment.name, workspace=\"default\")\n clear_output(wait=True)\n print(f\"Deployment status: {deployment_status.status}\")\n deployment_state = str(deployment_status.status).lower()\n if deployment_state in (\"ready\", \"running\"):\n if not sdk.models.wait_for_gateway(deployment.name, workspace=\"default\", timeout=60):\n raise RuntimeError(\"Inference gateway did not become ready\")\n break\n if deployment_state in (\"failed\", \"error\", \"terminated\", \"lost\"):\n raise RuntimeError(f\"Deployment failed with status: {deployment_status.status}\")\n time.sleep(15)", + "language": "python", + "source_html": "check = max_wait_time_checker(1800, "Deployment")\nwhile True:\n check()\n deployment_status = sdk.inference.deployments.retrieve(name=deployment.name, workspace="default")\n clear_output(wait=True)\n print(f"Deployment status: {deployment_status.status}")\n deployment_state = str(deployment_status.status).lower()\n if deployment_state in ("ready", "running"):\n if not sdk.models.wait_for_gateway(deployment.name, workspace="default", timeout=60):\n raise RuntimeError("Inference gateway did not become ready")\n break\n if deployment_state in ("failed", "error", "terminated", "lost"):\n raise RuntimeError(f"Deployment failed with status: {deployment_status.status}")\n time.sleep(15)\n" + }, + { + "type": "code", + "source": "messages = [\n {\"role\": \"system\", \"content\": \"You are a helpful assistant.\"},\n {\"role\": \"user\", \"content\": \"Write a short, friendly email to a colleague asking to reschedule our meeting to Thursday.\"},\n]\n\nresponse = sdk.inference.gateway.provider.post(\n \"v1/chat/completions\",\n name=deployment.name,\n workspace=\"default\",\n body={\"model\": f\"default/{OUTPUT_NAME}\", \"messages\": messages, \"temperature\": 0.7, \"max_tokens\": 256},\n)\nprint(\"Model output:\\n\")\nprint(response[\"choices\"][0][\"message\"][\"content\"])", + "language": "python", + "source_html": "messages = [\n {"role": "system", "content": "You are a helpful assistant."},\n {"role": "user", "content": "Write a short, friendly email to a colleague asking to reschedule our meeting to Thursday."},\n]\n\nresponse = sdk.inference.gateway.provider.post(\n "v1/chat/completions",\n name=deployment.name,\n workspace="default",\n body={"model": f"default/{OUTPUT_NAME}", "messages": messages, "temperature": 0.7, "max_tokens": 256},\n)\nprint("Model output:\\n")\nprint(response["choices"][0]["message"]["content"])\n" + }, + { + "type": "markdown", + "source": "## Conclusion\n\nYou aligned a base model with **DPO** on the NeMo Platform using the `rl` backend:\n\n- Uploaded a HelpSteer3 preference dataset **as-is** (the platform detects the schema natively).\n- Submitted a full-weight DPO job that ran on a Ray cluster via the Kubernetes executor.\n- Registered the output as a full model entity and (optionally) deployed it for inference.\n\n**Next steps:** tune the alignment strength with `ref_policy_kl_penalty` (β), add `sft_loss_weight` to anchor the policy to the chosen responses, enable `activation_checkpointing` for memory headroom, or scale up with `parallelism`. See the [Training Configuration](/documentation/customizer-reference/manage-customization-jobs/training-configuration) reference for the full hyperparameter set.", + "source_html": "

Conclusion

\n

You aligned a base model with DPO on the NeMo Platform using the rl backend:

\n
    \n
  • Uploaded a HelpSteer3 preference dataset as-is (the platform detects the schema natively).
  • \n
  • Submitted a full-weight DPO job that ran on a Ray cluster via the Kubernetes executor.
  • \n
  • Registered the output as a full model entity and (optionally) deployed it for inference.
  • \n
\n

Next steps: tune the alignment strength with ref_policy_kl_penalty (β), add sft_loss_weight to anchor the policy to the chosen responses, enable activation_checkpointing for memory headroom, or scale up with parallelism. See the Training Configuration reference for the full hyperparameter set.

\n" + } +] }; diff --git a/docs/fern/components/notebooks/embedding-customization-job.json b/docs/fern/components/notebooks/embedding-customization-job.json index fda1255a6e..82e62a10c9 100644 --- a/docs/fern/components/notebooks/embedding-customization-job.json +++ b/docs/fern/components/notebooks/embedding-customization-job.json @@ -7,8 +7,8 @@ }, { "type": "markdown", - "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **HuggingFace token** with read access to download the SPECTER dataset (get one at [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens))\n4. **NGC API key** to pull NIM container images from nvcr.io (get one at [ngc.nvidia.com](https://ngc.nvidia.com/) → Setup → Generate API Key)", - "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. HuggingFace token with read access to download the SPECTER dataset (get one at huggingface.co/settings/tokens)
  6. \n
  7. NGC API key to pull NIM container images from nvcr.io (get one at ngc.nvidia.com → Setup → Generate API Key)
  8. \n
\n" + "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **NGC API key** to pull NIM container images from nvcr.io (get one at [ngc.nvidia.com](https://ngc.nvidia.com/) → Setup → Generate API Key)\n4. **At least one GPU with CUDA 13+**\n\nThe SPECTER dataset and the tutorial's base model are public and do not require a Hugging Face token. If you substitute a gated or private model, provide a token with read access.", + "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. NGC API key to pull NIM container images from nvcr.io (get one at ngc.nvidia.com → Setup → Generate API Key)
  6. \n
  7. At least one GPU with CUDA 13+
  8. \n
\n

The SPECTER dataset and the tutorial's base model are public and do not require a Hugging Face token. If you substitute a gated or private model, provide a token with read access.

\n" }, { "type": "markdown", @@ -64,8 +64,8 @@ }, { "type": "markdown", - "source": "### 3. Prepare Dataset\n\nUse the [SPECTER dataset](https://huggingface.co/datasets/embedding-data/SPECTER) from HuggingFace, a collection of scientific paper triplets where papers that cite each other are considered related.\n\n**Dataset structure:**\n- ~684K scientific paper triplets (this tutorial uses 10%)\n- Each triplet: (query paper, positive/related paper, negative/unrelated paper)\n- Papers that cite each other are marked as \"related\"\n\nIn this tutorial the following dataset directory structure will be used:\n```\nembedding-dataset\n`-- training.jsonl\n`-- validation.jsonl\n```", - "source_html": "

3. Prepare Dataset

\n

Use the SPECTER dataset from HuggingFace, a collection of scientific paper triplets where papers that cite each other are considered related.

\n

Dataset structure:

\n
    \n
  • ~684K scientific paper triplets (this tutorial uses 10%)
  • \n
  • Each triplet: (query paper, positive/related paper, negative/unrelated paper)
  • \n
  • Papers that cite each other are marked as "related"
  • \n
\n

In this tutorial the following dataset directory structure will be used:

\n
embedding-dataset\n`-- training.jsonl\n`-- validation.jsonl\n
\n" + "source": "### 3. Prepare Dataset\n\nUse the [SPECTER dataset](https://huggingface.co/datasets/embedding-data/SPECTER) from Hugging Face, a collection of scientific paper triplets where papers that cite each other are considered related.\n\n**Dataset structure:**\n- ~684K scientific paper triplets (this tutorial uses 10%)\n- Each triplet: (query paper, positive/related paper, negative/unrelated paper)\n- Papers that cite each other are marked as \"related\"\n\nIn this tutorial the following dataset directory structure will be used:\n```\nembedding-dataset\n`-- training.jsonl\n`-- validation.jsonl\n```", + "source_html": "

3. Prepare Dataset

\n

Use the SPECTER dataset from Hugging Face, a collection of scientific paper triplets where papers that cite each other are considered related.

\n

Dataset structure:

\n
    \n
  • ~684K scientific paper triplets (this tutorial uses 10%)
  • \n
  • Each triplet: (query paper, positive/related paper, negative/unrelated paper)
  • \n
  • Papers that cite each other are marked as "related"
  • \n
\n

In this tutorial the following dataset directory structure will be used:

\n
embedding-dataset\n`-- training.jsonl\n`-- validation.jsonl\n
\n" }, { "type": "markdown", @@ -74,9 +74,9 @@ }, { "type": "code", - "source": "from pathlib import Path\nfrom datasets import load_dataset\nimport json\n\n# HuggingFace token for dataset access\nHF_TOKEN = os.environ.get(\"HF_TOKEN\")\nif not HF_TOKEN:\n raise ValueError(\"HF_TOKEN environment variable is required. Get one at https://huggingface.co/settings/tokens\")\nos.environ[\"HF_TOKEN\"] = HF_TOKEN\n\n# Configuration\nDATASET_SIZE = 3000 # Number of triplets (increase for better results, max ~684K)\nVALIDATION_SPLIT = 0.05 # 5% held out for validation\nSEED = 42\nDATASET_PATH = Path(\"embedding-dataset\").absolute()\n\n# Create directory\nos.makedirs(DATASET_PATH, exist_ok=True)\n\n# Download SPECTER dataset\nprint(\"Downloading SPECTER dataset...\")\ndata = load_dataset(\"embedding-data/SPECTER\")[\"train\"].shuffle(seed=SEED).select(range(DATASET_SIZE))\n\n# Split into train/validation\nprint(\"Splitting into train/validation...\")\nsplits = data.train_test_split(test_size=VALIDATION_SPLIT, seed=SEED)\ntrain_data = splits[\"train\"]\nvalidation_data = splits[\"test\"]\n\n# Convert to triplet JSONL format\nprint(\"Saving to JSONL...\")\nfor name, dataset in [(\"training\", train_data), (\"validation\", validation_data)]:\n with open(f\"{DATASET_PATH}/{name}.jsonl\", \"w\") as f:\n for row in dataset:\n # SPECTER format: row['set'] = [query, positive, negative]\n triplet = {\n \"query\": row[\"set\"][0],\n \"pos_doc\": row[\"set\"][1],\n \"neg_doc\": [row[\"set\"][2]] # List of negative documents\n }\n f.write(json.dumps(triplet) + \"\\n\")\n\nprint(f\"\\nPrepared {len(train_data):,} training, {len(validation_data):,} validation samples\")\nprint(f\"\\nExample triplet:\")\nprint(f\" Query: {train_data[0]['set'][0][:100]}...\")\nprint(f\" Positive: {train_data[0]['set'][1][:100]}...\")\nprint(f\" Negative: {train_data[0]['set'][2][:100]}...\")", + "source": "from pathlib import Path\nfrom datasets import load_dataset\nimport json\n\n# Configuration\nDATASET_SIZE = 3000 # Number of triplets (increase for better results, max ~684K)\nVALIDATION_SPLIT = 0.05 # 5% held out for validation\nSEED = 42\nDATASET_PATH = Path(\"embedding-dataset\").absolute()\n\n# Create directory\nos.makedirs(DATASET_PATH, exist_ok=True)\n\n# Download SPECTER dataset\nprint(\"Downloading SPECTER dataset...\")\ndata = load_dataset(\"embedding-data/SPECTER\")[\"train\"].shuffle(seed=SEED).select(range(DATASET_SIZE))\n\n# Split into train/validation\nprint(\"Splitting into train/validation...\")\nsplits = data.train_test_split(test_size=VALIDATION_SPLIT, seed=SEED)\ntrain_data = splits[\"train\"]\nvalidation_data = splits[\"test\"]\n\n# Convert to triplet JSONL format\nprint(\"Saving to JSONL...\")\nfor name, dataset in [(\"training\", train_data), (\"validation\", validation_data)]:\n with open(f\"{DATASET_PATH}/{name}.jsonl\", \"w\") as f:\n for row in dataset:\n # SPECTER format: row['set'] = [query, positive, negative]\n triplet = {\n \"query\": row[\"set\"][0],\n \"pos_doc\": row[\"set\"][1],\n \"neg_doc\": [row[\"set\"][2]] # List of negative documents\n }\n f.write(json.dumps(triplet) + \"\\n\")\n\nprint(f\"\\nPrepared {len(train_data):,} training, {len(validation_data):,} validation samples\")\nprint(f\"\\nExample triplet:\")\nprint(f\" Query: {train_data[0]['set'][0][:100]}...\")\nprint(f\" Positive: {train_data[0]['set'][1][:100]}...\")\nprint(f\" Negative: {train_data[0]['set'][2][:100]}...\")", "language": "python", - "source_html": "from pathlib import Path\nfrom datasets import load_dataset\nimport json\n\n# HuggingFace token for dataset access\nHF_TOKEN = os.environ.get("HF_TOKEN")\nif not HF_TOKEN:\n raise ValueError("HF_TOKEN environment variable is required. Get one at https://huggingface.co/settings/tokens")\nos.environ["HF_TOKEN"] = HF_TOKEN\n\n# Configuration\nDATASET_SIZE = 3000 # Number of triplets (increase for better results, max ~684K)\nVALIDATION_SPLIT = 0.05 # 5% held out for validation\nSEED = 42\nDATASET_PATH = Path("embedding-dataset").absolute()\n\n# Create directory\nos.makedirs(DATASET_PATH, exist_ok=True)\n\n# Download SPECTER dataset\nprint("Downloading SPECTER dataset...")\ndata = load_dataset("embedding-data/SPECTER")["train"].shuffle(seed=SEED).select(range(DATASET_SIZE))\n\n# Split into train/validation\nprint("Splitting into train/validation...")\nsplits = data.train_test_split(test_size=VALIDATION_SPLIT, seed=SEED)\ntrain_data = splits["train"]\nvalidation_data = splits["test"]\n\n# Convert to triplet JSONL format\nprint("Saving to JSONL...")\nfor name, dataset in [("training", train_data), ("validation", validation_data)]:\n with open(f"{DATASET_PATH}/{name}.jsonl", "w") as f:\n for row in dataset:\n # SPECTER format: row['set'] = [query, positive, negative]\n triplet = {\n "query": row["set"][0],\n "pos_doc": row["set"][1],\n "neg_doc": [row["set"][2]] # List of negative documents\n }\n f.write(json.dumps(triplet) + "\\n")\n\nprint(f"\\nPrepared {len(train_data):,} training, {len(validation_data):,} validation samples")\nprint(f"\\nExample triplet:")\nprint(f" Query: {train_data[0]['set'][0][:100]}...")\nprint(f" Positive: {train_data[0]['set'][1][:100]}...")\nprint(f" Negative: {train_data[0]['set'][2][:100]}...")\n" + "source_html": "from pathlib import Path\nfrom datasets import load_dataset\nimport json\n\n# Configuration\nDATASET_SIZE = 3000 # Number of triplets (increase for better results, max ~684K)\nVALIDATION_SPLIT = 0.05 # 5% held out for validation\nSEED = 42\nDATASET_PATH = Path("embedding-dataset").absolute()\n\n# Create directory\nos.makedirs(DATASET_PATH, exist_ok=True)\n\n# Download SPECTER dataset\nprint("Downloading SPECTER dataset...")\ndata = load_dataset("embedding-data/SPECTER")["train"].shuffle(seed=SEED).select(range(DATASET_SIZE))\n\n# Split into train/validation\nprint("Splitting into train/validation...")\nsplits = data.train_test_split(test_size=VALIDATION_SPLIT, seed=SEED)\ntrain_data = splits["train"]\nvalidation_data = splits["test"]\n\n# Convert to triplet JSONL format\nprint("Saving to JSONL...")\nfor name, dataset in [("training", train_data), ("validation", validation_data)]:\n with open(f"{DATASET_PATH}/{name}.jsonl", "w") as f:\n for row in dataset:\n # SPECTER format: row['set'] = [query, positive, negative]\n triplet = {\n "query": row["set"][0],\n "pos_doc": row["set"][1],\n "neg_doc": [row["set"][2]] # List of negative documents\n }\n f.write(json.dumps(triplet) + "\\n")\n\nprint(f"\\nPrepared {len(train_data):,} training, {len(validation_data):,} validation samples")\nprint(f"\\nExample triplet:")\nprint(f" Query: {train_data[0]['set'][0][:100]}...")\nprint(f" Positive: {train_data[0]['set'][1][:100]}...")\nprint(f" Negative: {train_data[0]['set'][2][:100]}...")\n" }, { "type": "markdown", @@ -91,30 +91,30 @@ }, { "type": "markdown", - "source": "### 6. Secrets Setup\n\nConfigure authentication for accessing base models:\n\n- **NGC models** (`ngc://` URIs): Requires NGC API key\n- **HuggingFace models** (`hf://` URIs): Requires HF token for gated/private models\n\nGet your credentials:\n- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n- [HuggingFace Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n\n---\n\n#### Quick Setup Example\n\nThis tutorial fine-tunes [nvidia/llama-nemotron-embed-1b-v2](https://huggingface.co/nvidia/llama-nemotron-embed-1b-v2), an NVIDIA embedding model optimized for question-answering and retrieval tasks.", - "source_html": "

6. Secrets Setup

\n

Configure authentication for accessing base models:

\n
    \n
  • NGC models (ngc:// URIs): Requires NGC API key
  • \n
  • HuggingFace models (hf:// URIs): Requires HF token for gated/private models
  • \n
\n

Get your credentials:

\n\n
\n

Quick Setup Example

\n

This tutorial fine-tunes nvidia/llama-nemotron-embed-1b-v2, an NVIDIA embedding model optimized for question-answering and retrieval tasks.

\n" + "source": "### 6. Secrets Setup\n\nConfigure authentication for accessing base models:\n\n- **NGC models** (`ngc://` URIs): Requires NGC API key\n- **Hugging Face models** (`hf://` URIs): Requires HF token for gated/private models\n\nGet your credentials:\n- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n- [Hugging Face Token](https://huggingface.co/settings/tokens) (Optional; needed only for a gated/private replacement model)\n\n---\n\n#### Quick Setup Example\n\nThis tutorial fine-tunes [nvidia/llama-nemotron-embed-1b-v2](https://huggingface.co/nvidia/llama-nemotron-embed-1b-v2), an NVIDIA embedding model optimized for question-answering and retrieval tasks.", + "source_html": "

6. Secrets Setup

\n

Configure authentication for accessing base models:

\n
    \n
  • NGC models (ngc:// URIs): Requires NGC API key
  • \n
  • Hugging Face models (hf:// URIs): Requires HF token for gated/private models
  • \n
\n

Get your credentials:

\n\n
\n

Quick Setup Example

\n

This tutorial fine-tunes nvidia/llama-nemotron-embed-1b-v2, an NVIDIA embedding model optimized for question-answering and retrieval tasks.

\n" }, { "type": "code", - "source": "# Create secrets for model access\n# Note: NGC_API_KEY secret was already created in the baseline step (Step 2)\nHF_TOKEN = os.getenv(\"HF_TOKEN\")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f\"{label} is not set\")\n try:\n secret = client.secrets.create(\n name=name,\n workspace=\"default\",\n value=value,\n )\n print(f\"Created secret: {name}\")\n return secret\n except ConflictError:\n print(f\"Secret '{name}' already exists, continuing...\")\n return client.secrets.retrieve(name=name, workspace=\"default\")\n\n\n# Create HuggingFace token secret (for downloading model from HF during training)\nhf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\nprint(f\"HF_TOKEN secret: {hf_secret.name}\")\n\n# NGC secret was already created in baseline step (Step 2), or use the platform default\nif \"NGC_SECRET_NAME\" not in globals():\n NGC_SECRET_NAME = \"ngc-api-key\"\nprint(f\"NGC_API_KEY secret: {NGC_SECRET_NAME}\")", + "source": "# Create secrets for model access\n# Note: NGC_API_KEY secret was already created in the baseline step (Step 2)\nHF_TOKEN = os.getenv(\"HF_TOKEN\")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f\"{label} is not set\")\n try:\n secret = client.secrets.create(\n name=name,\n workspace=\"default\",\n value=value,\n )\n print(f\"Created secret: {name}\")\n return secret\n except ConflictError:\n print(f\"Secret '{name}' already exists, continuing...\")\n return client.secrets.retrieve(name=name, workspace=\"default\")\n\n\n# Public Hugging Face models need no token. Create a secret only when HF_TOKEN is set.\nhf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\") if HF_TOKEN else None\nif hf_secret:\n print(f\"HF_TOKEN secret: {hf_secret.name}\")\n\n# NGC secret was already created in baseline step (Step 2), or use the platform default\nif \"NGC_SECRET_NAME\" not in globals():\n NGC_SECRET_NAME = \"ngc-api-key\"\nprint(f\"NGC_API_KEY secret: {NGC_SECRET_NAME}\")", "language": "python", - "source_html": "# Create secrets for model access\n# Note: NGC_API_KEY secret was already created in the baseline step (Step 2)\nHF_TOKEN = os.getenv("HF_TOKEN")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f"{label} is not set")\n try:\n secret = client.secrets.create(\n name=name,\n workspace="default",\n value=value,\n )\n print(f"Created secret: {name}")\n return secret\n except ConflictError:\n print(f"Secret '{name}' already exists, continuing...")\n return client.secrets.retrieve(name=name, workspace="default")\n\n\n# Create HuggingFace token secret (for downloading model from HF during training)\nhf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN")\nprint(f"HF_TOKEN secret: {hf_secret.name}")\n\n# NGC secret was already created in baseline step (Step 2), or use the platform default\nif "NGC_SECRET_NAME" not in globals():\n NGC_SECRET_NAME = "ngc-api-key"\nprint(f"NGC_API_KEY secret: {NGC_SECRET_NAME}")\n" + "source_html": "# Create secrets for model access\n# Note: NGC_API_KEY secret was already created in the baseline step (Step 2)\nHF_TOKEN = os.getenv("HF_TOKEN")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f"{label} is not set")\n try:\n secret = client.secrets.create(\n name=name,\n workspace="default",\n value=value,\n )\n print(f"Created secret: {name}")\n return secret\n except ConflictError:\n print(f"Secret '{name}' already exists, continuing...")\n return client.secrets.retrieve(name=name, workspace="default")\n\n\n# Public Hugging Face models need no token. Create a secret only when HF_TOKEN is set.\nhf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN") if HF_TOKEN else None\nif hf_secret:\n print(f"HF_TOKEN secret: {hf_secret.name}")\n\n# NGC secret was already created in baseline step (Step 2), or use the platform default\nif "NGC_SECRET_NAME" not in globals():\n NGC_SECRET_NAME = "ngc-api-key"\nprint(f"NGC_API_KEY secret: {NGC_SECRET_NAME}")\n" }, { "type": "markdown", - "source": "### 7. Create Base Model FileSet and Model Entity\n\nCreate a fileset pointing to the [nvidia/llama-nemotron-embed-1b-v2](https://huggingface.co/nvidia/llama-nemotron-embed-1b-v2) embedding model from HuggingFace, then create a Model Entity that references this fileset. Model downloading will take place at training time.", - "source_html": "

7. Create Base Model FileSet and Model Entity

\n

Create a fileset pointing to the nvidia/llama-nemotron-embed-1b-v2 embedding model from HuggingFace, then create a Model Entity that references this fileset. Model downloading will take place at training time.

\n" + "source": "### 7. Create Base Model FileSet and Model Entity\n\nCreate a fileset pointing to the [nvidia/llama-nemotron-embed-1b-v2](https://huggingface.co/nvidia/llama-nemotron-embed-1b-v2) embedding model from Hugging Face, then create a Model Entity that references this fileset. Model downloading will take place at training time.", + "source_html": "

7. Create Base Model FileSet and Model Entity

\n

Create a fileset pointing to the nvidia/llama-nemotron-embed-1b-v2 embedding model from Hugging Face, then create a Model Entity that references this fileset. Model downloading will take place at training time.

\n" }, { "type": "code", - "source": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = \"nvidia/llama-nemotron-embed-1b-v2\"\nMODEL_NAME = \"nv-nemotron-embed-1b-base\"\n\n# Ensure you have a HuggingFace token secret created\ntry:\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"NVIDIA Llama Nemotron Embed 1B v2 embedding model\",\n storage=HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\nexcept ConflictError as e:\n print(f\"Base model fileset already exists. Skipping creation.\")\n base_model_fs = client.files.filesets.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\n# Create Model Entity referencing the FileSet\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=True,\n )\n print(f\"Created Model Entity: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model already exists. Updating fileset if different.\")\n base_model = client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=True,\n )\n\nprint(f\"\\nBase model fileset: fileset://default/{base_model.name}\")\nprint(\"\\nBase model files:\")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace=\"default\").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint(\"\\nWaiting for ModelSpec to be populated...\")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds\")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\nprint(f\"ModelSpec populated: {base_model.spec}\")", + "source": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = \"nvidia/llama-nemotron-embed-1b-v2\"\nMODEL_NAME = \"nv-nemotron-embed-1b-base\"\n\nstorage_kwargs = {\n \"type\": \"huggingface\",\n \"repo_id\": HF_REPO_ID,\n \"repo_type\": \"model\",\n}\nif hf_secret:\n storage_kwargs[\"token_secret\"] = hf_secret.name\nstorage = HuggingfaceStorageConfigParam(**storage_kwargs)\n\ntry:\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"NVIDIA Llama Nemotron Embed 1B v2 embedding model\",\n storage=storage,\n )\nexcept ConflictError as e:\n print(f\"Base model fileset already exists. Skipping creation.\")\n base_model_fs = client.files.filesets.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\n# Create Model Entity referencing the FileSet\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=True,\n )\n print(f\"Created Model Entity: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model already exists. Updating fileset if different.\")\n base_model = client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=True,\n )\n\nprint(f\"\\nBase model fileset: fileset://default/{base_model.name}\")\nprint(\"\\nBase model files:\")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace=\"default\").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint(\"\\nWaiting for ModelSpec to be populated...\")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds\")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\nprint(f\"ModelSpec populated: {base_model.spec}\")", "language": "python", - "source_html": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = "nvidia/llama-nemotron-embed-1b-v2"\nMODEL_NAME = "nv-nemotron-embed-1b-base"\n\n# Ensure you have a HuggingFace token secret created\ntry:\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="NVIDIA Llama Nemotron Embed 1B v2 embedding model",\n storage=HuggingfaceStorageConfigParam(\n type="huggingface",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type="model",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\nexcept ConflictError as e:\n print(f"Base model fileset already exists. Skipping creation.")\n base_model_fs = client.files.filesets.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\n# Create Model Entity referencing the FileSet\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=True,\n )\n print(f"Created Model Entity: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model already exists. Updating fileset if different.")\n base_model = client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=True,\n )\n\nprint(f"\\nBase model fileset: fileset://default/{base_model.name}")\nprint("\\nBase model files:")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace="default").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint("\\nWaiting for ModelSpec to be populated...")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\nprint(f"ModelSpec populated: {base_model.spec}")\n" + "source_html": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = "nvidia/llama-nemotron-embed-1b-v2"\nMODEL_NAME = "nv-nemotron-embed-1b-base"\n\nstorage_kwargs = {\n "type": "huggingface",\n "repo_id": HF_REPO_ID,\n "repo_type": "model",\n}\nif hf_secret:\n storage_kwargs["token_secret"] = hf_secret.name\nstorage = HuggingfaceStorageConfigParam(**storage_kwargs)\n\ntry:\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="NVIDIA Llama Nemotron Embed 1B v2 embedding model",\n storage=storage,\n )\nexcept ConflictError as e:\n print(f"Base model fileset already exists. Skipping creation.")\n base_model_fs = client.files.filesets.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\n# Create Model Entity referencing the FileSet\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=True,\n )\n print(f"Created Model Entity: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model already exists. Updating fileset if different.")\n base_model = client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=True,\n )\n\nprint(f"\\nBase model fileset: fileset://default/{base_model.name}")\nprint("\\nBase model files:")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace="default").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint("\\nWaiting for ModelSpec to be populated...")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\nprint(f"ModelSpec populated: {base_model.spec}")\n" }, { "type": "markdown", - "source": "### 8. Create Embedding Fine-tuning Job\n\nCreate a customization job to fine-tune the embedding model using contrastive learning on the SPECTER dataset.\n\nSubmit to the **Automodel** backend using `AutomodelJobInput` with split `schedule`, `batch`, `optimizer`, and `parallelism` sections. Reference the model entity and dataset fileset by workspace/name (not `fileset://` URIs).\n\n**Key hyperparameters for embedding fine-tuning:**\n- **`training.training_type`**: `sft`\n- **`training.finetuning_type`**: `all_weights` for full fine-tuning, or `lora_merged` for merged LoRA\n- **`optimizer.learning_rate`**: Lower values (1e-6 to 5e-6) work well for embedding models\n- **`batch.global_batch_size`**: Larger batches improve contrastive learning (128-256 recommended)\n\n**NOTE:**\n\nNeMo Platform does not support unmerged LoRA adapters for embedding models because the embedding NIM requires ONNX format, which cannot represent standalone adapters. This notebook uses all-weights fine-tuning. For merged LoRA, set `finetuning_type` to `lora_merged`:\n\n```python\ntraining={\n \"training_type\": \"sft\",\n \"finetuning_type\": \"lora_merged\",\n \"lora\": {\"rank\": 16, \"alpha\": 32},\n \"max_seq_length\": MAX_SEQ_LENGTH,\n}\n```", - "source_html": "

8. Create Embedding Fine-tuning Job

\n

Create a customization job to fine-tune the embedding model using contrastive learning on the SPECTER dataset.

\n

Submit to the Automodel backend using AutomodelJobInput with split schedule, batch, optimizer, and parallelism sections. Reference the model entity and dataset fileset by workspace/name (not fileset:// URIs).

\n

Key hyperparameters for embedding fine-tuning:

\n
    \n
  • training.training_type: sft
  • \n
  • training.finetuning_type: all_weights for full fine-tuning, or lora_merged for merged LoRA
  • \n
  • optimizer.learning_rate: Lower values (1e-6 to 5e-6) work well for embedding models
  • \n
  • batch.global_batch_size: Larger batches improve contrastive learning (128-256 recommended)
  • \n
\n

NOTE:

\n

NeMo Platform does not support unmerged LoRA adapters for embedding models because the embedding NIM requires ONNX format, which cannot represent standalone adapters. This notebook uses all-weights fine-tuning. For merged LoRA, set finetuning_type to lora_merged:

\n
training={\n    "training_type": "sft",\n    "finetuning_type": "lora_merged",\n    "lora": {"rank": 16, "alpha": 32},\n    "max_seq_length": MAX_SEQ_LENGTH,\n}\n
\n" + "source": "### 8. Create Embedding Fine-tuning Job\n\nCreate a customization job to fine-tune the embedding model using contrastive learning on the SPECTER dataset.\n\nSubmit to the **Automodel** backend using `AutomodelJobInput` with split `schedule`, `batch`, `optimizer`, and `parallelism` sections. Reference the model entity and dataset fileset by workspace/name (not `fileset://` URIs).\n\n**Key hyperparameters for embedding fine-tuning:**\n- **`training.training_type`**: `sft`\n- **`training.finetuning_type`**: `all_weights` for full fine-tuning, or `lora_merged` for merged LoRA\n- **`optimizer.learning_rate`**: Lower values (1e-6 to 5e-6) work well for embedding models\n- **`batch.global_batch_size`**: Larger batches improve contrastive learning (128-256 recommended)\n\n**NOTE:**\n\nNeMo Platform does not support unmerged LoRA adapters for embedding models because the embedding NIM requires ONNX format, which cannot represent standalone adapters. This notebook uses all-weights fine-tuning. For merged LoRA, set `finetuning_type` to `lora_merged`:\n\n```python\ntraining={\n \"training_type\": \"sft\",\n \"finetuning_type\": \"lora_merged\",\n \"lora\": {\"rank\": 16, \"alpha\": 32},\n \"max_seq_length\": 512,\n}\n```", + "source_html": "

8. Create Embedding Fine-tuning Job

\n

Create a customization job to fine-tune the embedding model using contrastive learning on the SPECTER dataset.

\n

Submit to the Automodel backend using AutomodelJobInput with split schedule, batch, optimizer, and parallelism sections. Reference the model entity and dataset fileset by workspace/name (not fileset:// URIs).

\n

Key hyperparameters for embedding fine-tuning:

\n
    \n
  • training.training_type: sft
  • \n
  • training.finetuning_type: all_weights for full fine-tuning, or lora_merged for merged LoRA
  • \n
  • optimizer.learning_rate: Lower values (1e-6 to 5e-6) work well for embedding models
  • \n
  • batch.global_batch_size: Larger batches improve contrastive learning (128-256 recommended)
  • \n
\n

NOTE:

\n

NeMo Platform does not support unmerged LoRA adapters for embedding models because the embedding NIM requires ONNX format, which cannot represent standalone adapters. This notebook uses all-weights fine-tuning. For merged LoRA, set finetuning_type to lora_merged:

\n
training={\n    "training_type": "sft",\n    "finetuning_type": "lora_merged",\n    "lora": {"rank": 16, "alpha": 32},\n    "max_seq_length": 512,\n}\n
\n" }, { "type": "code", @@ -129,9 +129,9 @@ }, { "type": "code", - "source": "import time\nfrom IPython.display import clear_output\n\n# Poll job status every 10 seconds until completed\nwhile True:\n status = client.jobs.get_status(\n name=job.job.name,\n workspace=\"default\"\n )\n \n clear_output(wait=True)\n print(f\"Job Status: {status.model_dump_json(indent=2)}\")\n\n # Extract training progress from nested steps structure\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n\n for job_step in status.steps or []:\n if job_step.name == \"training\":\n for task in job_step.tasks or []:\n task_details = task.status_details or {}\n step = task_details.get(\"step\")\n max_steps = task_details.get(\"max_steps\")\n training_phase = task_details.get(\"phase\")\n break\n break\n\n if step is not None and max_steps is not None:\n progress_pct = (step / max_steps) * 100\n print(f\"Training Progress: Step {step}/{max_steps} ({progress_pct:.1f}%)\")\n if training_phase:\n print(f\"Training Phase: {training_phase}\")\n else:\n print(\"Training step not started yet or progress info not available\")\n \n # Exit loop when job is completed (or failed/cancelled)\n if status.status in (\"completed\", \"failed\", \"cancelled\", \"error\"):\n print(f\"\\nJob finished with status: {status.status}\")\n break\n \n time.sleep(10)", + "source": "import time\nfrom IPython.display import clear_output\n\n# Poll job status every 10 seconds until completed\nwhile True:\n status = client.jobs.get_status(\n name=job.job.name,\n workspace=\"default\"\n )\n \n clear_output(wait=True)\n print(f\"Job Status: {status.model_dump_json(indent=2)}\")\n\n # Extract training progress from nested steps structure\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n\n for job_step in status.steps or []:\n if job_step.name == \"training\":\n for task in job_step.tasks or []:\n task_details = task.status_details or {}\n step = task_details.get(\"step\")\n max_steps = task_details.get(\"max_steps\")\n training_phase = task_details.get(\"phase\")\n break\n break\n\n if step is not None and max_steps is not None:\n progress_pct = (step / max_steps) * 100\n print(f\"Training Progress: Step {step}/{max_steps} ({progress_pct:.1f}%)\")\n if training_phase:\n print(f\"Training Phase: {training_phase}\")\n else:\n print(\"Training step not started yet or progress info not available\")\n \n # Exit loop when job is completed (or failed/cancelled)\n if status.status in (\"completed\", \"failed\", \"cancelled\", \"error\"):\n print(f\"\\nJob finished with status: {status.status}\")\n break\n \n time.sleep(10)\n\nif status.status != \"completed\":\n raise RuntimeError(f\"Training job finished with status: {status.status}\")", "language": "python", - "source_html": "import time\nfrom IPython.display import clear_output\n\n# Poll job status every 10 seconds until completed\nwhile True:\n status = client.jobs.get_status(\n name=job.job.name,\n workspace="default"\n )\n \n clear_output(wait=True)\n print(f"Job Status: {status.model_dump_json(indent=2)}")\n\n # Extract training progress from nested steps structure\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n\n for job_step in status.steps or []:\n if job_step.name == "training":\n for task in job_step.tasks or []:\n task_details = task.status_details or {}\n step = task_details.get("step")\n max_steps = task_details.get("max_steps")\n training_phase = task_details.get("phase")\n break\n break\n\n if step is not None and max_steps is not None:\n progress_pct = (step / max_steps) * 100\n print(f"Training Progress: Step {step}/{max_steps} ({progress_pct:.1f}%)")\n if training_phase:\n print(f"Training Phase: {training_phase}")\n else:\n print("Training step not started yet or progress info not available")\n \n # Exit loop when job is completed (or failed/cancelled)\n if status.status in ("completed", "failed", "cancelled", "error"):\n print(f"\\nJob finished with status: {status.status}")\n break\n \n time.sleep(10)\n" + "source_html": "import time\nfrom IPython.display import clear_output\n\n# Poll job status every 10 seconds until completed\nwhile True:\n status = client.jobs.get_status(\n name=job.job.name,\n workspace="default"\n )\n \n clear_output(wait=True)\n print(f"Job Status: {status.model_dump_json(indent=2)}")\n\n # Extract training progress from nested steps structure\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n\n for job_step in status.steps or []:\n if job_step.name == "training":\n for task in job_step.tasks or []:\n task_details = task.status_details or {}\n step = task_details.get("step")\n max_steps = task_details.get("max_steps")\n training_phase = task_details.get("phase")\n break\n break\n\n if step is not None and max_steps is not None:\n progress_pct = (step / max_steps) * 100\n print(f"Training Progress: Step {step}/{max_steps} ({progress_pct:.1f}%)")\n if training_phase:\n print(f"Training Phase: {training_phase}")\n else:\n print("Training step not started yet or progress info not available")\n \n # Exit loop when job is completed (or failed/cancelled)\n if status.status in ("completed", "failed", "cancelled", "error"):\n print(f"\\nJob finished with status: {status.status}")\n break\n \n time.sleep(10)\n\nif status.status != "completed":\n raise RuntimeError(f"Training job finished with status: {status.status}")\n" }, { "type": "markdown", @@ -179,8 +179,8 @@ }, { "type": "markdown", - "source": "### Evaluation Best Practices\n\n**Manual Evaluation** (Recommended)\n- Test with real-world queries from your domain\n- Compare retrieval rankings before and after fine-tuning\n- Check that semantically similar items rank higher than keyword matches\n\n**What to look for:**\n- ✅ Relevant documents consistently rank in top positions\n- ✅ Keyword traps (like \"Random Forest\" vs \"Random Fields\") are handled correctly\n- ✅ Domain-specific terminology is understood\n- ❌ Unrelated documents with matching keywords do not rank high\n\n**Benchmark Evaluation**\n\nFor systematic evaluation, use the NeMo Evaluator service with retrieval benchmarks like SciDocs, BEIR, or MTEB. Refer to the [Evaluator documentation](../../evaluator/index.md) for details.\n\n---\n\n## Hyperparameters\n\nFor detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the [Hyperparameter Reference](../manage-customization-jobs/hyperparameters.md).\n\n**Embedding-Specific Recommendations:**\n\n| Parameter | Recommended | Notes |\n|-----------|-------------|-------|\n| `learning_rate` | 1e-6 to 5e-6 | Lower than standard SFT |\n| `batch_size` | 128-256 | Larger batches improve contrastive learning |\n| `max_seq_length` | 512 | Typical for embedding models |\n| `epochs` | 1-3 | Start small, increase if needed |\n\n---\n\n## Troubleshooting\n\n**Embeddings do not show improved retrieval:**\n- Verify dataset quality: triplets should have clear positive/negative distinctions\n- Use hard negatives: negatives should share some overlap with the query but not be relevant (easy negatives do not teach the model much)\n- Increase dataset size: 10K+ triplets recommended for meaningful improvement\n- Try more epochs: embedding models often need multiple passes\n- Lower learning rate: embedding models are sensitive to LR\n\n**Training loss not decreasing:**\n- Check triplet format: ensure `neg_doc` is a list even for single negatives\n- Verify hard negative quality: negatives should be challenging but clearly non-relevant\n- Increase batch size: contrastive learning benefits from larger batches\n\n**Deployment fails:**\n- Ensure you use the correct NIM image for embedding models\n- Verify sufficient GPU memory for the model size\n- Check deployment status: `client.inference.deployments.retrieve(name=deployment.name, workspace=\"default\")` and refer to platform logs for debugging\n\n## Next Steps\n\n- [Monitor training metrics](../manage-customization-jobs/get-job-status.md) in detail\n- [Evaluate your model](../../evaluator/index.md) with retrieval benchmarks\n- Integrate the fine-tuned embedding model into your RAG pipeline\n- Scale up training with the full SPECTER dataset (~684K triplets) for better results", - "source_html": "

Evaluation Best Practices

\n

Manual Evaluation (Recommended)

\n
    \n
  • Test with real-world queries from your domain
  • \n
  • Compare retrieval rankings before and after fine-tuning
  • \n
  • Check that semantically similar items rank higher than keyword matches
  • \n
\n

What to look for:

\n
    \n
  • ✅ Relevant documents consistently rank in top positions
  • \n
  • ✅ Keyword traps (like "Random Forest" vs "Random Fields") are handled correctly
  • \n
  • ✅ Domain-specific terminology is understood
  • \n
  • ❌ Unrelated documents with matching keywords do not rank high
  • \n
\n

Benchmark Evaluation

\n

For systematic evaluation, use the NeMo Evaluator service with retrieval benchmarks like SciDocs, BEIR, or MTEB. Refer to the Evaluator documentation for details.

\n
\n

Hyperparameters

\n

For detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the Hyperparameter Reference.

\n

Embedding-Specific Recommendations:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ParameterRecommendedNotes
learning_rate1e-6 to 5e-6Lower than standard SFT
batch_size128-256Larger batches improve contrastive learning
max_seq_length512Typical for embedding models
epochs1-3Start small, increase if needed
\n
\n

Troubleshooting

\n

Embeddings do not show improved retrieval:

\n
    \n
  • Verify dataset quality: triplets should have clear positive/negative distinctions
  • \n
  • Use hard negatives: negatives should share some overlap with the query but not be relevant (easy negatives do not teach the model much)
  • \n
  • Increase dataset size: 10K+ triplets recommended for meaningful improvement
  • \n
  • Try more epochs: embedding models often need multiple passes
  • \n
  • Lower learning rate: embedding models are sensitive to LR
  • \n
\n

Training loss not decreasing:

\n
    \n
  • Check triplet format: ensure neg_doc is a list even for single negatives
  • \n
  • Verify hard negative quality: negatives should be challenging but clearly non-relevant
  • \n
  • Increase batch size: contrastive learning benefits from larger batches
  • \n
\n

Deployment fails:

\n
    \n
  • Ensure you use the correct NIM image for embedding models
  • \n
  • Verify sufficient GPU memory for the model size
  • \n
  • Check deployment status: client.inference.deployments.retrieve(name=deployment.name, workspace="default") and refer to platform logs for debugging
  • \n
\n

Next Steps

\n
    \n
  • Monitor training metrics in detail
  • \n
  • Evaluate your model with retrieval benchmarks
  • \n
  • Integrate the fine-tuned embedding model into your RAG pipeline
  • \n
  • Scale up training with the full SPECTER dataset (~684K triplets) for better results
  • \n
\n" + "source": "### Evaluation Best Practices\n\n**Manual Evaluation** (Recommended)\n- Test with real-world queries from your domain\n- Compare retrieval rankings before and after fine-tuning\n- Check that semantically similar items rank higher than keyword matches\n\n**What to look for:**\n- ✅ Relevant documents consistently rank in top positions\n- ✅ Keyword traps (like \"Random Forest\" vs \"Random Fields\") are handled correctly\n- ✅ Domain-specific terminology is understood\n- ❌ Unrelated documents with matching keywords do not rank high\n\n**Benchmark Evaluation**\n\nFor systematic evaluation of end-to-end retrieval quality in a RAG pipeline, use the NeMo Evaluator [RAG metrics](../../evaluator/metrics/rag.md) (RAGAS `context_recall`, `context_precision`, and `context_relevance`).\n\n---\n\n## Hyperparameters\n\nFor detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the [Hyperparameter Reference](../manage-customization-jobs/hyperparameters.md).\n\n**Embedding-Specific Recommendations:**\n\n| Parameter | Recommended | Notes |\n|-----------|-------------|-------|\n| `optimizer.learning_rate` | 1e-6 to 5e-6 | Lower than standard SFT |\n| `batch.global_batch_size` | 128-256 | Larger batches improve contrastive learning |\n| `training.max_seq_length` | 512 | Typical for embedding models |\n| `schedule.epochs` | 1-3 | Start small, increase if needed |\n\n---\n\n## Troubleshooting\n\n**Embeddings do not show improved retrieval:**\n- Verify dataset quality: triplets should have clear positive/negative distinctions\n- Use hard negatives: negatives should share some overlap with the query but not be relevant (easy negatives do not teach the model much)\n- Increase dataset size: 10K+ triplets recommended for meaningful improvement\n- Try more epochs: embedding models often need multiple passes\n- Lower learning rate: embedding models are sensitive to LR\n\n**Training loss not decreasing:**\n- Check triplet format: ensure `neg_doc` is a list even for single negatives\n- Verify hard negative quality: negatives should be challenging but clearly non-relevant\n- Increase batch size: contrastive learning benefits from larger batches\n\n**Deployment fails:**\n- Ensure you use the correct NIM image for embedding models\n- Verify sufficient GPU memory for the model size\n- Check deployment status: `client.inference.deployments.retrieve(name=deployment.name, workspace=\"default\")` and refer to platform logs for debugging\n\n## Next Steps\n\n- [Monitor training metrics](../manage-customization-jobs/get-job-status.md) in detail\n- [Evaluate your model](../../evaluator/metrics/rag.md) with RAG metrics\n- Integrate the fine-tuned embedding model into your RAG pipeline\n- Scale up training with the full SPECTER dataset (~684K triplets) for better results", + "source_html": "

Evaluation Best Practices

\n

Manual Evaluation (Recommended)

\n
    \n
  • Test with real-world queries from your domain
  • \n
  • Compare retrieval rankings before and after fine-tuning
  • \n
  • Check that semantically similar items rank higher than keyword matches
  • \n
\n

What to look for:

\n
    \n
  • ✅ Relevant documents consistently rank in top positions
  • \n
  • ✅ Keyword traps (like "Random Forest" vs "Random Fields") are handled correctly
  • \n
  • ✅ Domain-specific terminology is understood
  • \n
  • ❌ Unrelated documents with matching keywords do not rank high
  • \n
\n

Benchmark Evaluation

\n

For systematic evaluation of end-to-end retrieval quality in a RAG pipeline, use the NeMo Evaluator RAG metrics (RAGAS context_recall, context_precision, and context_relevance).

\n
\n

Hyperparameters

\n

For detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the Hyperparameter Reference.

\n

Embedding-Specific Recommendations:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ParameterRecommendedNotes
optimizer.learning_rate1e-6 to 5e-6Lower than standard SFT
batch.global_batch_size128-256Larger batches improve contrastive learning
training.max_seq_length512Typical for embedding models
schedule.epochs1-3Start small, increase if needed
\n
\n

Troubleshooting

\n

Embeddings do not show improved retrieval:

\n
    \n
  • Verify dataset quality: triplets should have clear positive/negative distinctions
  • \n
  • Use hard negatives: negatives should share some overlap with the query but not be relevant (easy negatives do not teach the model much)
  • \n
  • Increase dataset size: 10K+ triplets recommended for meaningful improvement
  • \n
  • Try more epochs: embedding models often need multiple passes
  • \n
  • Lower learning rate: embedding models are sensitive to LR
  • \n
\n

Training loss not decreasing:

\n
    \n
  • Check triplet format: ensure neg_doc is a list even for single negatives
  • \n
  • Verify hard negative quality: negatives should be challenging but clearly non-relevant
  • \n
  • Increase batch size: contrastive learning benefits from larger batches
  • \n
\n

Deployment fails:

\n
    \n
  • Ensure you use the correct NIM image for embedding models
  • \n
  • Verify sufficient GPU memory for the model size
  • \n
  • Check deployment status: client.inference.deployments.retrieve(name=deployment.name, workspace="default") and refer to platform logs for debugging
  • \n
\n

Next Steps

\n
    \n
  • Monitor training metrics in detail
  • \n
  • Evaluate your model with RAG metrics
  • \n
  • Integrate the fine-tuned embedding model into your RAG pipeline
  • \n
  • Scale up training with the full SPECTER dataset (~684K triplets) for better results
  • \n
\n" } ] } \ No newline at end of file diff --git a/docs/fern/components/notebooks/embedding-customization-job.ts b/docs/fern/components/notebooks/embedding-customization-job.ts index 0efe18943f..50c25536cf 100644 --- a/docs/fern/components/notebooks/embedding-customization-job.ts +++ b/docs/fern/components/notebooks/embedding-customization-job.ts @@ -12,8 +12,8 @@ export default { cells: [ }, { "type": "markdown", - "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **HuggingFace token** with read access to download the SPECTER dataset (get one at [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens))\n4. **NGC API key** to pull NIM container images from nvcr.io (get one at [ngc.nvidia.com](https://ngc.nvidia.com/) → Setup → Generate API Key)", - "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. HuggingFace token with read access to download the SPECTER dataset (get one at huggingface.co/settings/tokens)
  6. \n
  7. NGC API key to pull NIM container images from nvcr.io (get one at ngc.nvidia.com → Setup → Generate API Key)
  8. \n
\n" + "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **NGC API key** to pull NIM container images from nvcr.io (get one at [ngc.nvidia.com](https://ngc.nvidia.com/) → Setup → Generate API Key)\n4. **At least one GPU with CUDA 13+**\n\nThe SPECTER dataset and the tutorial's base model are public and do not require a Hugging Face token. If you substitute a gated or private model, provide a token with read access.", + "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. NGC API key to pull NIM container images from nvcr.io (get one at ngc.nvidia.com → Setup → Generate API Key)
  6. \n
  7. At least one GPU with CUDA 13+
  8. \n
\n

The SPECTER dataset and the tutorial's base model are public and do not require a Hugging Face token. If you substitute a gated or private model, provide a token with read access.

\n" }, { "type": "markdown", @@ -69,8 +69,8 @@ export default { cells: [ }, { "type": "markdown", - "source": "### 3. Prepare Dataset\n\nUse the [SPECTER dataset](https://huggingface.co/datasets/embedding-data/SPECTER) from HuggingFace, a collection of scientific paper triplets where papers that cite each other are considered related.\n\n**Dataset structure:**\n- ~684K scientific paper triplets (this tutorial uses 10%)\n- Each triplet: (query paper, positive/related paper, negative/unrelated paper)\n- Papers that cite each other are marked as \"related\"\n\nIn this tutorial the following dataset directory structure will be used:\n```\nembedding-dataset\n`-- training.jsonl\n`-- validation.jsonl\n```", - "source_html": "

3. Prepare Dataset

\n

Use the SPECTER dataset from HuggingFace, a collection of scientific paper triplets where papers that cite each other are considered related.

\n

Dataset structure:

\n
    \n
  • ~684K scientific paper triplets (this tutorial uses 10%)
  • \n
  • Each triplet: (query paper, positive/related paper, negative/unrelated paper)
  • \n
  • Papers that cite each other are marked as "related"
  • \n
\n

In this tutorial the following dataset directory structure will be used:

\n
embedding-dataset\n`-- training.jsonl\n`-- validation.jsonl\n
\n" + "source": "### 3. Prepare Dataset\n\nUse the [SPECTER dataset](https://huggingface.co/datasets/embedding-data/SPECTER) from Hugging Face, a collection of scientific paper triplets where papers that cite each other are considered related.\n\n**Dataset structure:**\n- ~684K scientific paper triplets (this tutorial uses 10%)\n- Each triplet: (query paper, positive/related paper, negative/unrelated paper)\n- Papers that cite each other are marked as \"related\"\n\nIn this tutorial the following dataset directory structure will be used:\n```\nembedding-dataset\n`-- training.jsonl\n`-- validation.jsonl\n```", + "source_html": "

3. Prepare Dataset

\n

Use the SPECTER dataset from Hugging Face, a collection of scientific paper triplets where papers that cite each other are considered related.

\n

Dataset structure:

\n
    \n
  • ~684K scientific paper triplets (this tutorial uses 10%)
  • \n
  • Each triplet: (query paper, positive/related paper, negative/unrelated paper)
  • \n
  • Papers that cite each other are marked as "related"
  • \n
\n

In this tutorial the following dataset directory structure will be used:

\n
embedding-dataset\n`-- training.jsonl\n`-- validation.jsonl\n
\n" }, { "type": "markdown", @@ -79,9 +79,9 @@ export default { cells: [ }, { "type": "code", - "source": "from pathlib import Path\nfrom datasets import load_dataset\nimport json\n\n# HuggingFace token for dataset access\nHF_TOKEN = os.environ.get(\"HF_TOKEN\")\nif not HF_TOKEN:\n raise ValueError(\"HF_TOKEN environment variable is required. Get one at https://huggingface.co/settings/tokens\")\nos.environ[\"HF_TOKEN\"] = HF_TOKEN\n\n# Configuration\nDATASET_SIZE = 3000 # Number of triplets (increase for better results, max ~684K)\nVALIDATION_SPLIT = 0.05 # 5% held out for validation\nSEED = 42\nDATASET_PATH = Path(\"embedding-dataset\").absolute()\n\n# Create directory\nos.makedirs(DATASET_PATH, exist_ok=True)\n\n# Download SPECTER dataset\nprint(\"Downloading SPECTER dataset...\")\ndata = load_dataset(\"embedding-data/SPECTER\")[\"train\"].shuffle(seed=SEED).select(range(DATASET_SIZE))\n\n# Split into train/validation\nprint(\"Splitting into train/validation...\")\nsplits = data.train_test_split(test_size=VALIDATION_SPLIT, seed=SEED)\ntrain_data = splits[\"train\"]\nvalidation_data = splits[\"test\"]\n\n# Convert to triplet JSONL format\nprint(\"Saving to JSONL...\")\nfor name, dataset in [(\"training\", train_data), (\"validation\", validation_data)]:\n with open(f\"{DATASET_PATH}/{name}.jsonl\", \"w\") as f:\n for row in dataset:\n # SPECTER format: row['set'] = [query, positive, negative]\n triplet = {\n \"query\": row[\"set\"][0],\n \"pos_doc\": row[\"set\"][1],\n \"neg_doc\": [row[\"set\"][2]] # List of negative documents\n }\n f.write(json.dumps(triplet) + \"\\n\")\n\nprint(f\"\\nPrepared {len(train_data):,} training, {len(validation_data):,} validation samples\")\nprint(f\"\\nExample triplet:\")\nprint(f\" Query: {train_data[0]['set'][0][:100]}...\")\nprint(f\" Positive: {train_data[0]['set'][1][:100]}...\")\nprint(f\" Negative: {train_data[0]['set'][2][:100]}...\")", + "source": "from pathlib import Path\nfrom datasets import load_dataset\nimport json\n\n# Configuration\nDATASET_SIZE = 3000 # Number of triplets (increase for better results, max ~684K)\nVALIDATION_SPLIT = 0.05 # 5% held out for validation\nSEED = 42\nDATASET_PATH = Path(\"embedding-dataset\").absolute()\n\n# Create directory\nos.makedirs(DATASET_PATH, exist_ok=True)\n\n# Download SPECTER dataset\nprint(\"Downloading SPECTER dataset...\")\ndata = load_dataset(\"embedding-data/SPECTER\")[\"train\"].shuffle(seed=SEED).select(range(DATASET_SIZE))\n\n# Split into train/validation\nprint(\"Splitting into train/validation...\")\nsplits = data.train_test_split(test_size=VALIDATION_SPLIT, seed=SEED)\ntrain_data = splits[\"train\"]\nvalidation_data = splits[\"test\"]\n\n# Convert to triplet JSONL format\nprint(\"Saving to JSONL...\")\nfor name, dataset in [(\"training\", train_data), (\"validation\", validation_data)]:\n with open(f\"{DATASET_PATH}/{name}.jsonl\", \"w\") as f:\n for row in dataset:\n # SPECTER format: row['set'] = [query, positive, negative]\n triplet = {\n \"query\": row[\"set\"][0],\n \"pos_doc\": row[\"set\"][1],\n \"neg_doc\": [row[\"set\"][2]] # List of negative documents\n }\n f.write(json.dumps(triplet) + \"\\n\")\n\nprint(f\"\\nPrepared {len(train_data):,} training, {len(validation_data):,} validation samples\")\nprint(f\"\\nExample triplet:\")\nprint(f\" Query: {train_data[0]['set'][0][:100]}...\")\nprint(f\" Positive: {train_data[0]['set'][1][:100]}...\")\nprint(f\" Negative: {train_data[0]['set'][2][:100]}...\")", "language": "python", - "source_html": "from pathlib import Path\nfrom datasets import load_dataset\nimport json\n\n# HuggingFace token for dataset access\nHF_TOKEN = os.environ.get("HF_TOKEN")\nif not HF_TOKEN:\n raise ValueError("HF_TOKEN environment variable is required. Get one at https://huggingface.co/settings/tokens")\nos.environ["HF_TOKEN"] = HF_TOKEN\n\n# Configuration\nDATASET_SIZE = 3000 # Number of triplets (increase for better results, max ~684K)\nVALIDATION_SPLIT = 0.05 # 5% held out for validation\nSEED = 42\nDATASET_PATH = Path("embedding-dataset").absolute()\n\n# Create directory\nos.makedirs(DATASET_PATH, exist_ok=True)\n\n# Download SPECTER dataset\nprint("Downloading SPECTER dataset...")\ndata = load_dataset("embedding-data/SPECTER")["train"].shuffle(seed=SEED).select(range(DATASET_SIZE))\n\n# Split into train/validation\nprint("Splitting into train/validation...")\nsplits = data.train_test_split(test_size=VALIDATION_SPLIT, seed=SEED)\ntrain_data = splits["train"]\nvalidation_data = splits["test"]\n\n# Convert to triplet JSONL format\nprint("Saving to JSONL...")\nfor name, dataset in [("training", train_data), ("validation", validation_data)]:\n with open(f"{DATASET_PATH}/{name}.jsonl", "w") as f:\n for row in dataset:\n # SPECTER format: row['set'] = [query, positive, negative]\n triplet = {\n "query": row["set"][0],\n "pos_doc": row["set"][1],\n "neg_doc": [row["set"][2]] # List of negative documents\n }\n f.write(json.dumps(triplet) + "\\n")\n\nprint(f"\\nPrepared {len(train_data):,} training, {len(validation_data):,} validation samples")\nprint(f"\\nExample triplet:")\nprint(f" Query: {train_data[0]['set'][0][:100]}...")\nprint(f" Positive: {train_data[0]['set'][1][:100]}...")\nprint(f" Negative: {train_data[0]['set'][2][:100]}...")\n" + "source_html": "from pathlib import Path\nfrom datasets import load_dataset\nimport json\n\n# Configuration\nDATASET_SIZE = 3000 # Number of triplets (increase for better results, max ~684K)\nVALIDATION_SPLIT = 0.05 # 5% held out for validation\nSEED = 42\nDATASET_PATH = Path("embedding-dataset").absolute()\n\n# Create directory\nos.makedirs(DATASET_PATH, exist_ok=True)\n\n# Download SPECTER dataset\nprint("Downloading SPECTER dataset...")\ndata = load_dataset("embedding-data/SPECTER")["train"].shuffle(seed=SEED).select(range(DATASET_SIZE))\n\n# Split into train/validation\nprint("Splitting into train/validation...")\nsplits = data.train_test_split(test_size=VALIDATION_SPLIT, seed=SEED)\ntrain_data = splits["train"]\nvalidation_data = splits["test"]\n\n# Convert to triplet JSONL format\nprint("Saving to JSONL...")\nfor name, dataset in [("training", train_data), ("validation", validation_data)]:\n with open(f"{DATASET_PATH}/{name}.jsonl", "w") as f:\n for row in dataset:\n # SPECTER format: row['set'] = [query, positive, negative]\n triplet = {\n "query": row["set"][0],\n "pos_doc": row["set"][1],\n "neg_doc": [row["set"][2]] # List of negative documents\n }\n f.write(json.dumps(triplet) + "\\n")\n\nprint(f"\\nPrepared {len(train_data):,} training, {len(validation_data):,} validation samples")\nprint(f"\\nExample triplet:")\nprint(f" Query: {train_data[0]['set'][0][:100]}...")\nprint(f" Positive: {train_data[0]['set'][1][:100]}...")\nprint(f" Negative: {train_data[0]['set'][2][:100]}...")\n" }, { "type": "markdown", @@ -96,30 +96,30 @@ export default { cells: [ }, { "type": "markdown", - "source": "### 6. Secrets Setup\n\nConfigure authentication for accessing base models:\n\n- **NGC models** (`ngc://` URIs): Requires NGC API key\n- **HuggingFace models** (`hf://` URIs): Requires HF token for gated/private models\n\nGet your credentials:\n- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n- [HuggingFace Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n\n---\n\n#### Quick Setup Example\n\nThis tutorial fine-tunes [nvidia/llama-nemotron-embed-1b-v2](https://huggingface.co/nvidia/llama-nemotron-embed-1b-v2), an NVIDIA embedding model optimized for question-answering and retrieval tasks.", - "source_html": "

6. Secrets Setup

\n

Configure authentication for accessing base models:

\n
    \n
  • NGC models (ngc:// URIs): Requires NGC API key
  • \n
  • HuggingFace models (hf:// URIs): Requires HF token for gated/private models
  • \n
\n

Get your credentials:

\n\n
\n

Quick Setup Example

\n

This tutorial fine-tunes nvidia/llama-nemotron-embed-1b-v2, an NVIDIA embedding model optimized for question-answering and retrieval tasks.

\n" + "source": "### 6. Secrets Setup\n\nConfigure authentication for accessing base models:\n\n- **NGC models** (`ngc://` URIs): Requires NGC API key\n- **Hugging Face models** (`hf://` URIs): Requires HF token for gated/private models\n\nGet your credentials:\n- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n- [Hugging Face Token](https://huggingface.co/settings/tokens) (Optional; needed only for a gated/private replacement model)\n\n---\n\n#### Quick Setup Example\n\nThis tutorial fine-tunes [nvidia/llama-nemotron-embed-1b-v2](https://huggingface.co/nvidia/llama-nemotron-embed-1b-v2), an NVIDIA embedding model optimized for question-answering and retrieval tasks.", + "source_html": "

6. Secrets Setup

\n

Configure authentication for accessing base models:

\n
    \n
  • NGC models (ngc:// URIs): Requires NGC API key
  • \n
  • Hugging Face models (hf:// URIs): Requires HF token for gated/private models
  • \n
\n

Get your credentials:

\n\n
\n

Quick Setup Example

\n

This tutorial fine-tunes nvidia/llama-nemotron-embed-1b-v2, an NVIDIA embedding model optimized for question-answering and retrieval tasks.

\n" }, { "type": "code", - "source": "# Create secrets for model access\n# Note: NGC_API_KEY secret was already created in the baseline step (Step 2)\nHF_TOKEN = os.getenv(\"HF_TOKEN\")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f\"{label} is not set\")\n try:\n secret = client.secrets.create(\n name=name,\n workspace=\"default\",\n value=value,\n )\n print(f\"Created secret: {name}\")\n return secret\n except ConflictError:\n print(f\"Secret '{name}' already exists, continuing...\")\n return client.secrets.retrieve(name=name, workspace=\"default\")\n\n\n# Create HuggingFace token secret (for downloading model from HF during training)\nhf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\nprint(f\"HF_TOKEN secret: {hf_secret.name}\")\n\n# NGC secret was already created in baseline step (Step 2), or use the platform default\nif \"NGC_SECRET_NAME\" not in globals():\n NGC_SECRET_NAME = \"ngc-api-key\"\nprint(f\"NGC_API_KEY secret: {NGC_SECRET_NAME}\")", + "source": "# Create secrets for model access\n# Note: NGC_API_KEY secret was already created in the baseline step (Step 2)\nHF_TOKEN = os.getenv(\"HF_TOKEN\")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f\"{label} is not set\")\n try:\n secret = client.secrets.create(\n name=name,\n workspace=\"default\",\n value=value,\n )\n print(f\"Created secret: {name}\")\n return secret\n except ConflictError:\n print(f\"Secret '{name}' already exists, continuing...\")\n return client.secrets.retrieve(name=name, workspace=\"default\")\n\n\n# Public Hugging Face models need no token. Create a secret only when HF_TOKEN is set.\nhf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\") if HF_TOKEN else None\nif hf_secret:\n print(f\"HF_TOKEN secret: {hf_secret.name}\")\n\n# NGC secret was already created in baseline step (Step 2), or use the platform default\nif \"NGC_SECRET_NAME\" not in globals():\n NGC_SECRET_NAME = \"ngc-api-key\"\nprint(f\"NGC_API_KEY secret: {NGC_SECRET_NAME}\")", "language": "python", - "source_html": "# Create secrets for model access\n# Note: NGC_API_KEY secret was already created in the baseline step (Step 2)\nHF_TOKEN = os.getenv("HF_TOKEN")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f"{label} is not set")\n try:\n secret = client.secrets.create(\n name=name,\n workspace="default",\n value=value,\n )\n print(f"Created secret: {name}")\n return secret\n except ConflictError:\n print(f"Secret '{name}' already exists, continuing...")\n return client.secrets.retrieve(name=name, workspace="default")\n\n\n# Create HuggingFace token secret (for downloading model from HF during training)\nhf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN")\nprint(f"HF_TOKEN secret: {hf_secret.name}")\n\n# NGC secret was already created in baseline step (Step 2), or use the platform default\nif "NGC_SECRET_NAME" not in globals():\n NGC_SECRET_NAME = "ngc-api-key"\nprint(f"NGC_API_KEY secret: {NGC_SECRET_NAME}")\n" + "source_html": "# Create secrets for model access\n# Note: NGC_API_KEY secret was already created in the baseline step (Step 2)\nHF_TOKEN = os.getenv("HF_TOKEN")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f"{label} is not set")\n try:\n secret = client.secrets.create(\n name=name,\n workspace="default",\n value=value,\n )\n print(f"Created secret: {name}")\n return secret\n except ConflictError:\n print(f"Secret '{name}' already exists, continuing...")\n return client.secrets.retrieve(name=name, workspace="default")\n\n\n# Public Hugging Face models need no token. Create a secret only when HF_TOKEN is set.\nhf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN") if HF_TOKEN else None\nif hf_secret:\n print(f"HF_TOKEN secret: {hf_secret.name}")\n\n# NGC secret was already created in baseline step (Step 2), or use the platform default\nif "NGC_SECRET_NAME" not in globals():\n NGC_SECRET_NAME = "ngc-api-key"\nprint(f"NGC_API_KEY secret: {NGC_SECRET_NAME}")\n" }, { "type": "markdown", - "source": "### 7. Create Base Model FileSet and Model Entity\n\nCreate a fileset pointing to the [nvidia/llama-nemotron-embed-1b-v2](https://huggingface.co/nvidia/llama-nemotron-embed-1b-v2) embedding model from HuggingFace, then create a Model Entity that references this fileset. Model downloading will take place at training time.", - "source_html": "

7. Create Base Model FileSet and Model Entity

\n

Create a fileset pointing to the nvidia/llama-nemotron-embed-1b-v2 embedding model from HuggingFace, then create a Model Entity that references this fileset. Model downloading will take place at training time.

\n" + "source": "### 7. Create Base Model FileSet and Model Entity\n\nCreate a fileset pointing to the [nvidia/llama-nemotron-embed-1b-v2](https://huggingface.co/nvidia/llama-nemotron-embed-1b-v2) embedding model from Hugging Face, then create a Model Entity that references this fileset. Model downloading will take place at training time.", + "source_html": "

7. Create Base Model FileSet and Model Entity

\n

Create a fileset pointing to the nvidia/llama-nemotron-embed-1b-v2 embedding model from Hugging Face, then create a Model Entity that references this fileset. Model downloading will take place at training time.

\n" }, { "type": "code", - "source": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = \"nvidia/llama-nemotron-embed-1b-v2\"\nMODEL_NAME = \"nv-nemotron-embed-1b-base\"\n\n# Ensure you have a HuggingFace token secret created\ntry:\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"NVIDIA Llama Nemotron Embed 1B v2 embedding model\",\n storage=HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\nexcept ConflictError as e:\n print(f\"Base model fileset already exists. Skipping creation.\")\n base_model_fs = client.files.filesets.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\n# Create Model Entity referencing the FileSet\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=True,\n )\n print(f\"Created Model Entity: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model already exists. Updating fileset if different.\")\n base_model = client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=True,\n )\n\nprint(f\"\\nBase model fileset: fileset://default/{base_model.name}\")\nprint(\"\\nBase model files:\")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace=\"default\").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint(\"\\nWaiting for ModelSpec to be populated...\")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds\")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\nprint(f\"ModelSpec populated: {base_model.spec}\")", + "source": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = \"nvidia/llama-nemotron-embed-1b-v2\"\nMODEL_NAME = \"nv-nemotron-embed-1b-base\"\n\nstorage_kwargs = {\n \"type\": \"huggingface\",\n \"repo_id\": HF_REPO_ID,\n \"repo_type\": \"model\",\n}\nif hf_secret:\n storage_kwargs[\"token_secret\"] = hf_secret.name\nstorage = HuggingfaceStorageConfigParam(**storage_kwargs)\n\ntry:\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"NVIDIA Llama Nemotron Embed 1B v2 embedding model\",\n storage=storage,\n )\nexcept ConflictError as e:\n print(f\"Base model fileset already exists. Skipping creation.\")\n base_model_fs = client.files.filesets.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\n# Create Model Entity referencing the FileSet\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=True,\n )\n print(f\"Created Model Entity: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model already exists. Updating fileset if different.\")\n base_model = client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=True,\n )\n\nprint(f\"\\nBase model fileset: fileset://default/{base_model.name}\")\nprint(\"\\nBase model files:\")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace=\"default\").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint(\"\\nWaiting for ModelSpec to be populated...\")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds\")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\nprint(f\"ModelSpec populated: {base_model.spec}\")", "language": "python", - "source_html": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = "nvidia/llama-nemotron-embed-1b-v2"\nMODEL_NAME = "nv-nemotron-embed-1b-base"\n\n# Ensure you have a HuggingFace token secret created\ntry:\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="NVIDIA Llama Nemotron Embed 1B v2 embedding model",\n storage=HuggingfaceStorageConfigParam(\n type="huggingface",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type="model",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\nexcept ConflictError as e:\n print(f"Base model fileset already exists. Skipping creation.")\n base_model_fs = client.files.filesets.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\n# Create Model Entity referencing the FileSet\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=True,\n )\n print(f"Created Model Entity: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model already exists. Updating fileset if different.")\n base_model = client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=True,\n )\n\nprint(f"\\nBase model fileset: fileset://default/{base_model.name}")\nprint("\\nBase model files:")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace="default").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint("\\nWaiting for ModelSpec to be populated...")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\nprint(f"ModelSpec populated: {base_model.spec}")\n" + "source_html": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = "nvidia/llama-nemotron-embed-1b-v2"\nMODEL_NAME = "nv-nemotron-embed-1b-base"\n\nstorage_kwargs = {\n "type": "huggingface",\n "repo_id": HF_REPO_ID,\n "repo_type": "model",\n}\nif hf_secret:\n storage_kwargs["token_secret"] = hf_secret.name\nstorage = HuggingfaceStorageConfigParam(**storage_kwargs)\n\ntry:\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="NVIDIA Llama Nemotron Embed 1B v2 embedding model",\n storage=storage,\n )\nexcept ConflictError as e:\n print(f"Base model fileset already exists. Skipping creation.")\n base_model_fs = client.files.filesets.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\n# Create Model Entity referencing the FileSet\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=True,\n )\n print(f"Created Model Entity: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model already exists. Updating fileset if different.")\n base_model = client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=True,\n )\n\nprint(f"\\nBase model fileset: fileset://default/{base_model.name}")\nprint("\\nBase model files:")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace="default").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint("\\nWaiting for ModelSpec to be populated...")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\nprint(f"ModelSpec populated: {base_model.spec}")\n" }, { "type": "markdown", - "source": "### 8. Create Embedding Fine-tuning Job\n\nCreate a customization job to fine-tune the embedding model using contrastive learning on the SPECTER dataset.\n\nSubmit to the **Automodel** backend using `AutomodelJobInput` with split `schedule`, `batch`, `optimizer`, and `parallelism` sections. Reference the model entity and dataset fileset by workspace/name (not `fileset://` URIs).\n\n**Key hyperparameters for embedding fine-tuning:**\n- **`training.training_type`**: `sft`\n- **`training.finetuning_type`**: `all_weights` for full fine-tuning, or `lora_merged` for merged LoRA\n- **`optimizer.learning_rate`**: Lower values (1e-6 to 5e-6) work well for embedding models\n- **`batch.global_batch_size`**: Larger batches improve contrastive learning (128-256 recommended)\n\n**NOTE:**\n\nNeMo Platform does not support unmerged LoRA adapters for embedding models because the embedding NIM requires ONNX format, which cannot represent standalone adapters. This notebook uses all-weights fine-tuning. For merged LoRA, set `finetuning_type` to `lora_merged`:\n\n```python\ntraining={\n \"training_type\": \"sft\",\n \"finetuning_type\": \"lora_merged\",\n \"lora\": {\"rank\": 16, \"alpha\": 32},\n \"max_seq_length\": MAX_SEQ_LENGTH,\n}\n```", - "source_html": "

8. Create Embedding Fine-tuning Job

\n

Create a customization job to fine-tune the embedding model using contrastive learning on the SPECTER dataset.

\n

Submit to the Automodel backend using AutomodelJobInput with split schedule, batch, optimizer, and parallelism sections. Reference the model entity and dataset fileset by workspace/name (not fileset:// URIs).

\n

Key hyperparameters for embedding fine-tuning:

\n
    \n
  • training.training_type: sft
  • \n
  • training.finetuning_type: all_weights for full fine-tuning, or lora_merged for merged LoRA
  • \n
  • optimizer.learning_rate: Lower values (1e-6 to 5e-6) work well for embedding models
  • \n
  • batch.global_batch_size: Larger batches improve contrastive learning (128-256 recommended)
  • \n
\n

NOTE:

\n

NeMo Platform does not support unmerged LoRA adapters for embedding models because the embedding NIM requires ONNX format, which cannot represent standalone adapters. This notebook uses all-weights fine-tuning. For merged LoRA, set finetuning_type to lora_merged:

\n
training={\n    "training_type": "sft",\n    "finetuning_type": "lora_merged",\n    "lora": {"rank": 16, "alpha": 32},\n    "max_seq_length": MAX_SEQ_LENGTH,\n}\n
\n" + "source": "### 8. Create Embedding Fine-tuning Job\n\nCreate a customization job to fine-tune the embedding model using contrastive learning on the SPECTER dataset.\n\nSubmit to the **Automodel** backend using `AutomodelJobInput` with split `schedule`, `batch`, `optimizer`, and `parallelism` sections. Reference the model entity and dataset fileset by workspace/name (not `fileset://` URIs).\n\n**Key hyperparameters for embedding fine-tuning:**\n- **`training.training_type`**: `sft`\n- **`training.finetuning_type`**: `all_weights` for full fine-tuning, or `lora_merged` for merged LoRA\n- **`optimizer.learning_rate`**: Lower values (1e-6 to 5e-6) work well for embedding models\n- **`batch.global_batch_size`**: Larger batches improve contrastive learning (128-256 recommended)\n\n**NOTE:**\n\nNeMo Platform does not support unmerged LoRA adapters for embedding models because the embedding NIM requires ONNX format, which cannot represent standalone adapters. This notebook uses all-weights fine-tuning. For merged LoRA, set `finetuning_type` to `lora_merged`:\n\n```python\ntraining={\n \"training_type\": \"sft\",\n \"finetuning_type\": \"lora_merged\",\n \"lora\": {\"rank\": 16, \"alpha\": 32},\n \"max_seq_length\": 512,\n}\n```", + "source_html": "

8. Create Embedding Fine-tuning Job

\n

Create a customization job to fine-tune the embedding model using contrastive learning on the SPECTER dataset.

\n

Submit to the Automodel backend using AutomodelJobInput with split schedule, batch, optimizer, and parallelism sections. Reference the model entity and dataset fileset by workspace/name (not fileset:// URIs).

\n

Key hyperparameters for embedding fine-tuning:

\n
    \n
  • training.training_type: sft
  • \n
  • training.finetuning_type: all_weights for full fine-tuning, or lora_merged for merged LoRA
  • \n
  • optimizer.learning_rate: Lower values (1e-6 to 5e-6) work well for embedding models
  • \n
  • batch.global_batch_size: Larger batches improve contrastive learning (128-256 recommended)
  • \n
\n

NOTE:

\n

NeMo Platform does not support unmerged LoRA adapters for embedding models because the embedding NIM requires ONNX format, which cannot represent standalone adapters. This notebook uses all-weights fine-tuning. For merged LoRA, set finetuning_type to lora_merged:

\n
training={\n    "training_type": "sft",\n    "finetuning_type": "lora_merged",\n    "lora": {"rank": 16, "alpha": 32},\n    "max_seq_length": 512,\n}\n
\n" }, { "type": "code", @@ -134,9 +134,9 @@ export default { cells: [ }, { "type": "code", - "source": "import time\nfrom IPython.display import clear_output\n\n# Poll job status every 10 seconds until completed\nwhile True:\n status = client.jobs.get_status(\n name=job.job.name,\n workspace=\"default\"\n )\n \n clear_output(wait=True)\n print(f\"Job Status: {status.model_dump_json(indent=2)}\")\n\n # Extract training progress from nested steps structure\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n\n for job_step in status.steps or []:\n if job_step.name == \"training\":\n for task in job_step.tasks or []:\n task_details = task.status_details or {}\n step = task_details.get(\"step\")\n max_steps = task_details.get(\"max_steps\")\n training_phase = task_details.get(\"phase\")\n break\n break\n\n if step is not None and max_steps is not None:\n progress_pct = (step / max_steps) * 100\n print(f\"Training Progress: Step {step}/{max_steps} ({progress_pct:.1f}%)\")\n if training_phase:\n print(f\"Training Phase: {training_phase}\")\n else:\n print(\"Training step not started yet or progress info not available\")\n \n # Exit loop when job is completed (or failed/cancelled)\n if status.status in (\"completed\", \"failed\", \"cancelled\", \"error\"):\n print(f\"\\nJob finished with status: {status.status}\")\n break\n \n time.sleep(10)", + "source": "import time\nfrom IPython.display import clear_output\n\n# Poll job status every 10 seconds until completed\nwhile True:\n status = client.jobs.get_status(\n name=job.job.name,\n workspace=\"default\"\n )\n \n clear_output(wait=True)\n print(f\"Job Status: {status.model_dump_json(indent=2)}\")\n\n # Extract training progress from nested steps structure\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n\n for job_step in status.steps or []:\n if job_step.name == \"training\":\n for task in job_step.tasks or []:\n task_details = task.status_details or {}\n step = task_details.get(\"step\")\n max_steps = task_details.get(\"max_steps\")\n training_phase = task_details.get(\"phase\")\n break\n break\n\n if step is not None and max_steps is not None:\n progress_pct = (step / max_steps) * 100\n print(f\"Training Progress: Step {step}/{max_steps} ({progress_pct:.1f}%)\")\n if training_phase:\n print(f\"Training Phase: {training_phase}\")\n else:\n print(\"Training step not started yet or progress info not available\")\n \n # Exit loop when job is completed (or failed/cancelled)\n if status.status in (\"completed\", \"failed\", \"cancelled\", \"error\"):\n print(f\"\\nJob finished with status: {status.status}\")\n break\n \n time.sleep(10)\n\nif status.status != \"completed\":\n raise RuntimeError(f\"Training job finished with status: {status.status}\")", "language": "python", - "source_html": "import time\nfrom IPython.display import clear_output\n\n# Poll job status every 10 seconds until completed\nwhile True:\n status = client.jobs.get_status(\n name=job.job.name,\n workspace="default"\n )\n \n clear_output(wait=True)\n print(f"Job Status: {status.model_dump_json(indent=2)}")\n\n # Extract training progress from nested steps structure\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n\n for job_step in status.steps or []:\n if job_step.name == "training":\n for task in job_step.tasks or []:\n task_details = task.status_details or {}\n step = task_details.get("step")\n max_steps = task_details.get("max_steps")\n training_phase = task_details.get("phase")\n break\n break\n\n if step is not None and max_steps is not None:\n progress_pct = (step / max_steps) * 100\n print(f"Training Progress: Step {step}/{max_steps} ({progress_pct:.1f}%)")\n if training_phase:\n print(f"Training Phase: {training_phase}")\n else:\n print("Training step not started yet or progress info not available")\n \n # Exit loop when job is completed (or failed/cancelled)\n if status.status in ("completed", "failed", "cancelled", "error"):\n print(f"\\nJob finished with status: {status.status}")\n break\n \n time.sleep(10)\n" + "source_html": "import time\nfrom IPython.display import clear_output\n\n# Poll job status every 10 seconds until completed\nwhile True:\n status = client.jobs.get_status(\n name=job.job.name,\n workspace="default"\n )\n \n clear_output(wait=True)\n print(f"Job Status: {status.model_dump_json(indent=2)}")\n\n # Extract training progress from nested steps structure\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n\n for job_step in status.steps or []:\n if job_step.name == "training":\n for task in job_step.tasks or []:\n task_details = task.status_details or {}\n step = task_details.get("step")\n max_steps = task_details.get("max_steps")\n training_phase = task_details.get("phase")\n break\n break\n\n if step is not None and max_steps is not None:\n progress_pct = (step / max_steps) * 100\n print(f"Training Progress: Step {step}/{max_steps} ({progress_pct:.1f}%)")\n if training_phase:\n print(f"Training Phase: {training_phase}")\n else:\n print("Training step not started yet or progress info not available")\n \n # Exit loop when job is completed (or failed/cancelled)\n if status.status in ("completed", "failed", "cancelled", "error"):\n print(f"\\nJob finished with status: {status.status}")\n break\n \n time.sleep(10)\n\nif status.status != "completed":\n raise RuntimeError(f"Training job finished with status: {status.status}")\n" }, { "type": "markdown", @@ -184,7 +184,7 @@ export default { cells: [ }, { "type": "markdown", - "source": "### Evaluation Best Practices\n\n**Manual Evaluation** (Recommended)\n- Test with real-world queries from your domain\n- Compare retrieval rankings before and after fine-tuning\n- Check that semantically similar items rank higher than keyword matches\n\n**What to look for:**\n- ✅ Relevant documents consistently rank in top positions\n- ✅ Keyword traps (like \"Random Forest\" vs \"Random Fields\") are handled correctly\n- ✅ Domain-specific terminology is understood\n- ❌ Unrelated documents with matching keywords do not rank high\n\n**Benchmark Evaluation**\n\nFor systematic evaluation, use the NeMo Evaluator service with retrieval benchmarks like SciDocs, BEIR, or MTEB. Refer to the [Evaluator documentation](../../evaluator/index.md) for details.\n\n---\n\n## Hyperparameters\n\nFor detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the [Hyperparameter Reference](../manage-customization-jobs/hyperparameters.md).\n\n**Embedding-Specific Recommendations:**\n\n| Parameter | Recommended | Notes |\n|-----------|-------------|-------|\n| `learning_rate` | 1e-6 to 5e-6 | Lower than standard SFT |\n| `batch_size` | 128-256 | Larger batches improve contrastive learning |\n| `max_seq_length` | 512 | Typical for embedding models |\n| `epochs` | 1-3 | Start small, increase if needed |\n\n---\n\n## Troubleshooting\n\n**Embeddings do not show improved retrieval:**\n- Verify dataset quality: triplets should have clear positive/negative distinctions\n- Use hard negatives: negatives should share some overlap with the query but not be relevant (easy negatives do not teach the model much)\n- Increase dataset size: 10K+ triplets recommended for meaningful improvement\n- Try more epochs: embedding models often need multiple passes\n- Lower learning rate: embedding models are sensitive to LR\n\n**Training loss not decreasing:**\n- Check triplet format: ensure `neg_doc` is a list even for single negatives\n- Verify hard negative quality: negatives should be challenging but clearly non-relevant\n- Increase batch size: contrastive learning benefits from larger batches\n\n**Deployment fails:**\n- Ensure you use the correct NIM image for embedding models\n- Verify sufficient GPU memory for the model size\n- Check deployment status: `client.inference.deployments.retrieve(name=deployment.name, workspace=\"default\")` and refer to platform logs for debugging\n\n## Next Steps\n\n- [Monitor training metrics](../manage-customization-jobs/get-job-status.md) in detail\n- [Evaluate your model](../../evaluator/index.md) with retrieval benchmarks\n- Integrate the fine-tuned embedding model into your RAG pipeline\n- Scale up training with the full SPECTER dataset (~684K triplets) for better results", - "source_html": "

Evaluation Best Practices

\n

Manual Evaluation (Recommended)

\n
    \n
  • Test with real-world queries from your domain
  • \n
  • Compare retrieval rankings before and after fine-tuning
  • \n
  • Check that semantically similar items rank higher than keyword matches
  • \n
\n

What to look for:

\n
    \n
  • ✅ Relevant documents consistently rank in top positions
  • \n
  • ✅ Keyword traps (like "Random Forest" vs "Random Fields") are handled correctly
  • \n
  • ✅ Domain-specific terminology is understood
  • \n
  • ❌ Unrelated documents with matching keywords do not rank high
  • \n
\n

Benchmark Evaluation

\n

For systematic evaluation, use the NeMo Evaluator service with retrieval benchmarks like SciDocs, BEIR, or MTEB. Refer to the Evaluator documentation for details.

\n
\n

Hyperparameters

\n

For detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the Hyperparameter Reference.

\n

Embedding-Specific Recommendations:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ParameterRecommendedNotes
learning_rate1e-6 to 5e-6Lower than standard SFT
batch_size128-256Larger batches improve contrastive learning
max_seq_length512Typical for embedding models
epochs1-3Start small, increase if needed
\n
\n

Troubleshooting

\n

Embeddings do not show improved retrieval:

\n
    \n
  • Verify dataset quality: triplets should have clear positive/negative distinctions
  • \n
  • Use hard negatives: negatives should share some overlap with the query but not be relevant (easy negatives do not teach the model much)
  • \n
  • Increase dataset size: 10K+ triplets recommended for meaningful improvement
  • \n
  • Try more epochs: embedding models often need multiple passes
  • \n
  • Lower learning rate: embedding models are sensitive to LR
  • \n
\n

Training loss not decreasing:

\n
    \n
  • Check triplet format: ensure neg_doc is a list even for single negatives
  • \n
  • Verify hard negative quality: negatives should be challenging but clearly non-relevant
  • \n
  • Increase batch size: contrastive learning benefits from larger batches
  • \n
\n

Deployment fails:

\n
    \n
  • Ensure you use the correct NIM image for embedding models
  • \n
  • Verify sufficient GPU memory for the model size
  • \n
  • Check deployment status: client.inference.deployments.retrieve(name=deployment.name, workspace="default") and refer to platform logs for debugging
  • \n
\n

Next Steps

\n
    \n
  • Monitor training metrics in detail
  • \n
  • Evaluate your model with retrieval benchmarks
  • \n
  • Integrate the fine-tuned embedding model into your RAG pipeline
  • \n
  • Scale up training with the full SPECTER dataset (~684K triplets) for better results
  • \n
\n" + "source": "### Evaluation Best Practices\n\n**Manual Evaluation** (Recommended)\n- Test with real-world queries from your domain\n- Compare retrieval rankings before and after fine-tuning\n- Check that semantically similar items rank higher than keyword matches\n\n**What to look for:**\n- ✅ Relevant documents consistently rank in top positions\n- ✅ Keyword traps (like \"Random Forest\" vs \"Random Fields\") are handled correctly\n- ✅ Domain-specific terminology is understood\n- ❌ Unrelated documents with matching keywords do not rank high\n\n**Benchmark Evaluation**\n\nFor systematic evaluation of end-to-end retrieval quality in a RAG pipeline, use the NeMo Evaluator [RAG metrics](../../evaluator/metrics/rag.md) (RAGAS `context_recall`, `context_precision`, and `context_relevance`).\n\n---\n\n## Hyperparameters\n\nFor detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the [Hyperparameter Reference](../manage-customization-jobs/hyperparameters.md).\n\n**Embedding-Specific Recommendations:**\n\n| Parameter | Recommended | Notes |\n|-----------|-------------|-------|\n| `optimizer.learning_rate` | 1e-6 to 5e-6 | Lower than standard SFT |\n| `batch.global_batch_size` | 128-256 | Larger batches improve contrastive learning |\n| `training.max_seq_length` | 512 | Typical for embedding models |\n| `schedule.epochs` | 1-3 | Start small, increase if needed |\n\n---\n\n## Troubleshooting\n\n**Embeddings do not show improved retrieval:**\n- Verify dataset quality: triplets should have clear positive/negative distinctions\n- Use hard negatives: negatives should share some overlap with the query but not be relevant (easy negatives do not teach the model much)\n- Increase dataset size: 10K+ triplets recommended for meaningful improvement\n- Try more epochs: embedding models often need multiple passes\n- Lower learning rate: embedding models are sensitive to LR\n\n**Training loss not decreasing:**\n- Check triplet format: ensure `neg_doc` is a list even for single negatives\n- Verify hard negative quality: negatives should be challenging but clearly non-relevant\n- Increase batch size: contrastive learning benefits from larger batches\n\n**Deployment fails:**\n- Ensure you use the correct NIM image for embedding models\n- Verify sufficient GPU memory for the model size\n- Check deployment status: `client.inference.deployments.retrieve(name=deployment.name, workspace=\"default\")` and refer to platform logs for debugging\n\n## Next Steps\n\n- [Monitor training metrics](../manage-customization-jobs/get-job-status.md) in detail\n- [Evaluate your model](../../evaluator/metrics/rag.md) with RAG metrics\n- Integrate the fine-tuned embedding model into your RAG pipeline\n- Scale up training with the full SPECTER dataset (~684K triplets) for better results", + "source_html": "

Evaluation Best Practices

\n

Manual Evaluation (Recommended)

\n
    \n
  • Test with real-world queries from your domain
  • \n
  • Compare retrieval rankings before and after fine-tuning
  • \n
  • Check that semantically similar items rank higher than keyword matches
  • \n
\n

What to look for:

\n
    \n
  • ✅ Relevant documents consistently rank in top positions
  • \n
  • ✅ Keyword traps (like "Random Forest" vs "Random Fields") are handled correctly
  • \n
  • ✅ Domain-specific terminology is understood
  • \n
  • ❌ Unrelated documents with matching keywords do not rank high
  • \n
\n

Benchmark Evaluation

\n

For systematic evaluation of end-to-end retrieval quality in a RAG pipeline, use the NeMo Evaluator RAG metrics (RAGAS context_recall, context_precision, and context_relevance).

\n
\n

Hyperparameters

\n

For detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the Hyperparameter Reference.

\n

Embedding-Specific Recommendations:

\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n
ParameterRecommendedNotes
optimizer.learning_rate1e-6 to 5e-6Lower than standard SFT
batch.global_batch_size128-256Larger batches improve contrastive learning
training.max_seq_length512Typical for embedding models
schedule.epochs1-3Start small, increase if needed
\n
\n

Troubleshooting

\n

Embeddings do not show improved retrieval:

\n
    \n
  • Verify dataset quality: triplets should have clear positive/negative distinctions
  • \n
  • Use hard negatives: negatives should share some overlap with the query but not be relevant (easy negatives do not teach the model much)
  • \n
  • Increase dataset size: 10K+ triplets recommended for meaningful improvement
  • \n
  • Try more epochs: embedding models often need multiple passes
  • \n
  • Lower learning rate: embedding models are sensitive to LR
  • \n
\n

Training loss not decreasing:

\n
    \n
  • Check triplet format: ensure neg_doc is a list even for single negatives
  • \n
  • Verify hard negative quality: negatives should be challenging but clearly non-relevant
  • \n
  • Increase batch size: contrastive learning benefits from larger batches
  • \n
\n

Deployment fails:

\n
    \n
  • Ensure you use the correct NIM image for embedding models
  • \n
  • Verify sufficient GPU memory for the model size
  • \n
  • Check deployment status: client.inference.deployments.retrieve(name=deployment.name, workspace="default") and refer to platform logs for debugging
  • \n
\n

Next Steps

\n
    \n
  • Monitor training metrics in detail
  • \n
  • Evaluate your model with RAG metrics
  • \n
  • Integrate the fine-tuned embedding model into your RAG pipeline
  • \n
  • Scale up training with the full SPECTER dataset (~684K triplets) for better results
  • \n
\n" } ] }; diff --git a/docs/fern/components/notebooks/lora-customization-job.json b/docs/fern/components/notebooks/lora-customization-job.json index e2f2ff8b16..502ece99c3 100644 --- a/docs/fern/components/notebooks/lora-customization-job.json +++ b/docs/fern/components/notebooks/lora-customization-job.json @@ -7,8 +7,8 @@ }, { "type": "markdown", - "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **Installed the `datasets` package** for loading SQuAD: `pip install datasets`\n4. **At least one GPU with CUDA 12.8+**", - "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. Installed the datasets package for loading SQuAD: pip install datasets
  6. \n
  7. At least one GPU with CUDA 12.8+
  8. \n
\n" + "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **Installed the `datasets` package** for loading SQuAD: `pip install datasets`\n4. **At least one GPU with CUDA 13+**", + "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. Installed the datasets package for loading SQuAD: pip install datasets
  6. \n
  7. At least one GPU with CUDA 13+
  8. \n
\n" }, { "type": "markdown", @@ -55,8 +55,8 @@ }, { "type": "markdown", - "source": "### 4. Secrets Setup\n\nFor Huggingface models that require authentication, create a secret with your HF token. Get a token from [Huggingface Settings](https://huggingface.co/settings/tokens) and accept the model terms.\n\nThis is generally true for LLaMa based models (e.g. [Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)).\n\n```sh\nexport HF_TOKEN=\n```", - "source_html": "

4. Secrets Setup

\n

For Huggingface models that require authentication, create a secret with your HF token. Get a token from Huggingface Settings and accept the model terms.

\n

This is generally true for LLaMa based models (e.g. Llama-3.2-1B-Instruct).

\n
export HF_TOKEN=<your-huggingface-token>\n
\n" + "source": "### 4. Secrets Setup\n\nFor Hugging Face models that require authentication, create a secret with your HF token. Get a token from [Hugging Face Settings](https://huggingface.co/settings/tokens) and accept the model terms.\n\nThis is generally true for Llama-based models (for example, [Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)).\n\n```sh\nexport HF_TOKEN=\n```", + "source_html": "

4. Secrets Setup

\n

For Hugging Face models that require authentication, create a secret with your HF token. Get a token from Hugging Face Settings and accept the model terms.

\n

This is generally true for Llama-based models (for example, Llama-3.2-1B-Instruct).

\n
export HF_TOKEN=<your-huggingface-token>\n
\n" }, { "type": "code", @@ -71,9 +71,9 @@ }, { "type": "code", - "source": "HF_REPO_ID = \"Qwen/Qwen3-0.6B\"\nMODEL_NAME = \"qwen3-0.6b\"\n\ntry:\n storage = HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n )\n if hf_secret:\n storage[\"token_secret\"] = hf_secret.name\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"Qwen3 0.6b base model from Huggingface\",\n storage=storage,\n cache=True,\n )\nexcept ConflictError:\n base_model_fs = client.files.filesets.retrieve(workspace=\"default\", name=MODEL_NAME)\n\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=False,\n )\nexcept ConflictError:\n client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=False,\n )\n base_model = client.models.retrieve(workspace=\"default\", name=MODEL_NAME)\n\nprint(f\"Base model fileset: fileset://default/{base_model.name}\")\nprint(client.files.list(fileset=MODEL_NAME, workspace=\"default\"))\n\ntime_check = max_wait_time_checker(600, \"Model Spec\")\nwhile not base_model.spec:\n time_check()\n time.sleep(10)\n base_model = client.models.retrieve(workspace=\"default\", name=MODEL_NAME)\n\n# Clear verbose linear_layers list for cleaner output\nbase_model.spec.linear_layers = None\nprint(f\"ModelSpec: {base_model.spec}\")", + "source": "HF_REPO_ID = \"Qwen/Qwen3-0.6B\"\nMODEL_NAME = \"qwen3-0.6b\"\n\ntry:\n storage = HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n )\n if hf_secret:\n storage[\"token_secret\"] = hf_secret.name\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"Qwen3 0.6b base model from Hugging Face\",\n storage=storage,\n cache=True,\n )\nexcept ConflictError:\n base_model_fs = client.files.filesets.retrieve(workspace=\"default\", name=MODEL_NAME)\n\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=False,\n )\nexcept ConflictError:\n client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=False,\n )\n base_model = client.models.retrieve(workspace=\"default\", name=MODEL_NAME)\n\nprint(f\"Base model fileset: fileset://default/{base_model.name}\")\nprint(client.files.list(fileset=MODEL_NAME, workspace=\"default\"))\n\ntime_check = max_wait_time_checker(600, \"Model Spec\")\nwhile not base_model.spec:\n time_check()\n time.sleep(10)\n base_model = client.models.retrieve(workspace=\"default\", name=MODEL_NAME)\n\n# Clear verbose linear_layers list for cleaner output\nbase_model.spec.linear_layers = None\nprint(f\"ModelSpec: {base_model.spec}\")", "language": "python", - "source_html": "HF_REPO_ID = "Qwen/Qwen3-0.6B"\nMODEL_NAME = "qwen3-0.6b"\n\ntry:\n storage = HuggingfaceStorageConfigParam(\n type="huggingface",\n repo_id=HF_REPO_ID,\n repo_type="model",\n )\n if hf_secret:\n storage["token_secret"] = hf_secret.name\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="Qwen3 0.6b base model from Huggingface",\n storage=storage,\n cache=True,\n )\nexcept ConflictError:\n base_model_fs = client.files.filesets.retrieve(workspace="default", name=MODEL_NAME)\n\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=False,\n )\nexcept ConflictError:\n client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=False,\n )\n base_model = client.models.retrieve(workspace="default", name=MODEL_NAME)\n\nprint(f"Base model fileset: fileset://default/{base_model.name}")\nprint(client.files.list(fileset=MODEL_NAME, workspace="default"))\n\ntime_check = max_wait_time_checker(600, "Model Spec")\nwhile not base_model.spec:\n time_check()\n time.sleep(10)\n base_model = client.models.retrieve(workspace="default", name=MODEL_NAME)\n\n# Clear verbose linear_layers list for cleaner output\nbase_model.spec.linear_layers = None\nprint(f"ModelSpec: {base_model.spec}")\n" + "source_html": "HF_REPO_ID = "Qwen/Qwen3-0.6B"\nMODEL_NAME = "qwen3-0.6b"\n\ntry:\n storage = HuggingfaceStorageConfigParam(\n type="huggingface",\n repo_id=HF_REPO_ID,\n repo_type="model",\n )\n if hf_secret:\n storage["token_secret"] = hf_secret.name\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="Qwen3 0.6b base model from Hugging Face",\n storage=storage,\n cache=True,\n )\nexcept ConflictError:\n base_model_fs = client.files.filesets.retrieve(workspace="default", name=MODEL_NAME)\n\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=False,\n )\nexcept ConflictError:\n client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=False,\n )\n base_model = client.models.retrieve(workspace="default", name=MODEL_NAME)\n\nprint(f"Base model fileset: fileset://default/{base_model.name}")\nprint(client.files.list(fileset=MODEL_NAME, workspace="default"))\n\ntime_check = max_wait_time_checker(600, "Model Spec")\nwhile not base_model.spec:\n time_check()\n time.sleep(10)\n base_model = client.models.retrieve(workspace="default", name=MODEL_NAME)\n\n# Clear verbose linear_layers list for cleaner output\nbase_model.spec.linear_layers = None\nprint(f"ModelSpec: {base_model.spec}")\n" }, { "type": "markdown", @@ -126,9 +126,9 @@ }, { "type": "code", - "source": "context = \"The Apollo 11 mission was the first manned mission to land on the Moon. It was launched on July 16, 1969, and Neil Armstrong became the first person to walk on the lunar surface on July 20, 1969. Buzz Aldrin joined him shortly after, while Michael Collins remained in lunar orbit.\"\nquestion = \"Who was the first person to walk on the Moon?\"\nmessages = [\n {\"role\": \"user\", \"content\": f\"Based on the following context, answer the question.\\n\\nContext: {context}\\n\\nQuestion: {question}\"}\n]\nresponse = client.inference.gateway.provider.post(\n \"v1/chat/completions\",\n name=deployment_name,\n workspace=\"default\",\n body={\n \"model\": OUTPUT_NAME,\n \"messages\": messages,\n \"temperature\": 0,\n \"max_tokens\": 256,\n }\n)\nprint(\"=\" * 60)\nprint(\"MODEL INFERENCE\")\nprint(\"=\" * 60)\nprint(f\"Question: {question}\")\nprint(f\"Expected: Neil Armstrong\")\nprint(f\"Model output: {response['choices'][0]['message']['content']}\")", + "source": "context = \"The Apollo 11 mission was the first manned mission to land on the Moon. It was launched on July 16, 1969, and Neil Armstrong became the first person to walk on the lunar surface on July 20, 1969. Buzz Aldrin joined him shortly after, while Michael Collins remained in lunar orbit.\"\nquestion = \"Who was the first person to walk on the Moon?\"\nmessages = [\n {\"role\": \"user\", \"content\": f\"Based on the following context, answer the question.\\n\\nContext: {context}\\n\\nQuestion: {question}\"}\n]\nINFERENCE_MODEL_NAME = f\"default--{OUTPUT_NAME}\"\nresponse = client.inference.gateway.provider.post(\n \"v1/chat/completions\",\n name=deployment_name,\n workspace=\"default\",\n body={\n \"model\": INFERENCE_MODEL_NAME,\n \"messages\": messages,\n \"temperature\": 0,\n \"max_tokens\": 256,\n }\n)\nprint(\"=\" * 60)\nprint(\"MODEL INFERENCE\")\nprint(\"=\" * 60)\nprint(f\"Question: {question}\")\nprint(f\"Expected: Neil Armstrong\")\nprint(f\"Model output: {response['choices'][0]['message']['content']}\")", "language": "python", - "source_html": "context = "The Apollo 11 mission was the first manned mission to land on the Moon. It was launched on July 16, 1969, and Neil Armstrong became the first person to walk on the lunar surface on July 20, 1969. Buzz Aldrin joined him shortly after, while Michael Collins remained in lunar orbit."\nquestion = "Who was the first person to walk on the Moon?"\nmessages = [\n {"role": "user", "content": f"Based on the following context, answer the question.\\n\\nContext: {context}\\n\\nQuestion: {question}"}\n]\nresponse = client.inference.gateway.provider.post(\n "v1/chat/completions",\n name=deployment_name,\n workspace="default",\n body={\n "model": OUTPUT_NAME,\n "messages": messages,\n "temperature": 0,\n "max_tokens": 256,\n }\n)\nprint("=" * 60)\nprint("MODEL INFERENCE")\nprint("=" * 60)\nprint(f"Question: {question}")\nprint(f"Expected: Neil Armstrong")\nprint(f"Model output: {response['choices'][0]['message']['content']}")\n" + "source_html": "context = "The Apollo 11 mission was the first manned mission to land on the Moon. It was launched on July 16, 1969, and Neil Armstrong became the first person to walk on the lunar surface on July 20, 1969. Buzz Aldrin joined him shortly after, while Michael Collins remained in lunar orbit."\nquestion = "Who was the first person to walk on the Moon?"\nmessages = [\n {"role": "user", "content": f"Based on the following context, answer the question.\\n\\nContext: {context}\\n\\nQuestion: {question}"}\n]\nINFERENCE_MODEL_NAME = f"default--{OUTPUT_NAME}"\nresponse = client.inference.gateway.provider.post(\n "v1/chat/completions",\n name=deployment_name,\n workspace="default",\n body={\n "model": INFERENCE_MODEL_NAME,\n "messages": messages,\n "temperature": 0,\n "max_tokens": 256,\n }\n)\nprint("=" * 60)\nprint("MODEL INFERENCE")\nprint("=" * 60)\nprint(f"Question: {question}")\nprint(f"Expected: Neil Armstrong")\nprint(f"Model output: {response['choices'][0]['message']['content']}")\n" }, { "type": "markdown", diff --git a/docs/fern/components/notebooks/lora-customization-job.ts b/docs/fern/components/notebooks/lora-customization-job.ts index d4b0272cd4..dd7691c96a 100644 --- a/docs/fern/components/notebooks/lora-customization-job.ts +++ b/docs/fern/components/notebooks/lora-customization-job.ts @@ -12,8 +12,8 @@ export default { cells: [ }, { "type": "markdown", - "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **Installed the `datasets` package** for loading SQuAD: `pip install datasets`\n4. **At least one GPU with CUDA 12.8+**", - "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. Installed the datasets package for loading SQuAD: pip install datasets
  6. \n
  7. At least one GPU with CUDA 12.8+
  8. \n
\n" + "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **Installed the `datasets` package** for loading SQuAD: `pip install datasets`\n4. **At least one GPU with CUDA 13+**", + "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. Installed the datasets package for loading SQuAD: pip install datasets
  6. \n
  7. At least one GPU with CUDA 13+
  8. \n
\n" }, { "type": "markdown", @@ -60,8 +60,8 @@ export default { cells: [ }, { "type": "markdown", - "source": "### 4. Secrets Setup\n\nFor Huggingface models that require authentication, create a secret with your HF token. Get a token from [Huggingface Settings](https://huggingface.co/settings/tokens) and accept the model terms.\n\nThis is generally true for LLaMa based models (e.g. [Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)).\n\n```sh\nexport HF_TOKEN=\n```", - "source_html": "

4. Secrets Setup

\n

For Huggingface models that require authentication, create a secret with your HF token. Get a token from Huggingface Settings and accept the model terms.

\n

This is generally true for LLaMa based models (e.g. Llama-3.2-1B-Instruct).

\n
export HF_TOKEN=<your-huggingface-token>\n
\n" + "source": "### 4. Secrets Setup\n\nFor Hugging Face models that require authentication, create a secret with your HF token. Get a token from [Hugging Face Settings](https://huggingface.co/settings/tokens) and accept the model terms.\n\nThis is generally true for Llama-based models (for example, [Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct)).\n\n```sh\nexport HF_TOKEN=\n```", + "source_html": "

4. Secrets Setup

\n

For Hugging Face models that require authentication, create a secret with your HF token. Get a token from Hugging Face Settings and accept the model terms.

\n

This is generally true for Llama-based models (for example, Llama-3.2-1B-Instruct).

\n
export HF_TOKEN=<your-huggingface-token>\n
\n" }, { "type": "code", @@ -76,9 +76,9 @@ export default { cells: [ }, { "type": "code", - "source": "HF_REPO_ID = \"Qwen/Qwen3-0.6B\"\nMODEL_NAME = \"qwen3-0.6b\"\n\ntry:\n storage = HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n )\n if hf_secret:\n storage[\"token_secret\"] = hf_secret.name\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"Qwen3 0.6b base model from Huggingface\",\n storage=storage,\n cache=True,\n )\nexcept ConflictError:\n base_model_fs = client.files.filesets.retrieve(workspace=\"default\", name=MODEL_NAME)\n\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=False,\n )\nexcept ConflictError:\n client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=False,\n )\n base_model = client.models.retrieve(workspace=\"default\", name=MODEL_NAME)\n\nprint(f\"Base model fileset: fileset://default/{base_model.name}\")\nprint(client.files.list(fileset=MODEL_NAME, workspace=\"default\"))\n\ntime_check = max_wait_time_checker(600, \"Model Spec\")\nwhile not base_model.spec:\n time_check()\n time.sleep(10)\n base_model = client.models.retrieve(workspace=\"default\", name=MODEL_NAME)\n\n# Clear verbose linear_layers list for cleaner output\nbase_model.spec.linear_layers = None\nprint(f\"ModelSpec: {base_model.spec}\")", + "source": "HF_REPO_ID = \"Qwen/Qwen3-0.6B\"\nMODEL_NAME = \"qwen3-0.6b\"\n\ntry:\n storage = HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n )\n if hf_secret:\n storage[\"token_secret\"] = hf_secret.name\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"Qwen3 0.6b base model from Hugging Face\",\n storage=storage,\n cache=True,\n )\nexcept ConflictError:\n base_model_fs = client.files.filesets.retrieve(workspace=\"default\", name=MODEL_NAME)\n\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=False,\n )\nexcept ConflictError:\n client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n trust_remote_code=False,\n )\n base_model = client.models.retrieve(workspace=\"default\", name=MODEL_NAME)\n\nprint(f\"Base model fileset: fileset://default/{base_model.name}\")\nprint(client.files.list(fileset=MODEL_NAME, workspace=\"default\"))\n\ntime_check = max_wait_time_checker(600, \"Model Spec\")\nwhile not base_model.spec:\n time_check()\n time.sleep(10)\n base_model = client.models.retrieve(workspace=\"default\", name=MODEL_NAME)\n\n# Clear verbose linear_layers list for cleaner output\nbase_model.spec.linear_layers = None\nprint(f\"ModelSpec: {base_model.spec}\")", "language": "python", - "source_html": "HF_REPO_ID = "Qwen/Qwen3-0.6B"\nMODEL_NAME = "qwen3-0.6b"\n\ntry:\n storage = HuggingfaceStorageConfigParam(\n type="huggingface",\n repo_id=HF_REPO_ID,\n repo_type="model",\n )\n if hf_secret:\n storage["token_secret"] = hf_secret.name\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="Qwen3 0.6b base model from Huggingface",\n storage=storage,\n cache=True,\n )\nexcept ConflictError:\n base_model_fs = client.files.filesets.retrieve(workspace="default", name=MODEL_NAME)\n\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=False,\n )\nexcept ConflictError:\n client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=False,\n )\n base_model = client.models.retrieve(workspace="default", name=MODEL_NAME)\n\nprint(f"Base model fileset: fileset://default/{base_model.name}")\nprint(client.files.list(fileset=MODEL_NAME, workspace="default"))\n\ntime_check = max_wait_time_checker(600, "Model Spec")\nwhile not base_model.spec:\n time_check()\n time.sleep(10)\n base_model = client.models.retrieve(workspace="default", name=MODEL_NAME)\n\n# Clear verbose linear_layers list for cleaner output\nbase_model.spec.linear_layers = None\nprint(f"ModelSpec: {base_model.spec}")\n" + "source_html": "HF_REPO_ID = "Qwen/Qwen3-0.6B"\nMODEL_NAME = "qwen3-0.6b"\n\ntry:\n storage = HuggingfaceStorageConfigParam(\n type="huggingface",\n repo_id=HF_REPO_ID,\n repo_type="model",\n )\n if hf_secret:\n storage["token_secret"] = hf_secret.name\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="Qwen3 0.6b base model from Hugging Face",\n storage=storage,\n cache=True,\n )\nexcept ConflictError:\n base_model_fs = client.files.filesets.retrieve(workspace="default", name=MODEL_NAME)\n\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=False,\n )\nexcept ConflictError:\n client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n trust_remote_code=False,\n )\n base_model = client.models.retrieve(workspace="default", name=MODEL_NAME)\n\nprint(f"Base model fileset: fileset://default/{base_model.name}")\nprint(client.files.list(fileset=MODEL_NAME, workspace="default"))\n\ntime_check = max_wait_time_checker(600, "Model Spec")\nwhile not base_model.spec:\n time_check()\n time.sleep(10)\n base_model = client.models.retrieve(workspace="default", name=MODEL_NAME)\n\n# Clear verbose linear_layers list for cleaner output\nbase_model.spec.linear_layers = None\nprint(f"ModelSpec: {base_model.spec}")\n" }, { "type": "markdown", @@ -131,9 +131,9 @@ export default { cells: [ }, { "type": "code", - "source": "context = \"The Apollo 11 mission was the first manned mission to land on the Moon. It was launched on July 16, 1969, and Neil Armstrong became the first person to walk on the lunar surface on July 20, 1969. Buzz Aldrin joined him shortly after, while Michael Collins remained in lunar orbit.\"\nquestion = \"Who was the first person to walk on the Moon?\"\nmessages = [\n {\"role\": \"user\", \"content\": f\"Based on the following context, answer the question.\\n\\nContext: {context}\\n\\nQuestion: {question}\"}\n]\nresponse = client.inference.gateway.provider.post(\n \"v1/chat/completions\",\n name=deployment_name,\n workspace=\"default\",\n body={\n \"model\": OUTPUT_NAME,\n \"messages\": messages,\n \"temperature\": 0,\n \"max_tokens\": 256,\n }\n)\nprint(\"=\" * 60)\nprint(\"MODEL INFERENCE\")\nprint(\"=\" * 60)\nprint(f\"Question: {question}\")\nprint(f\"Expected: Neil Armstrong\")\nprint(f\"Model output: {response['choices'][0]['message']['content']}\")", + "source": "context = \"The Apollo 11 mission was the first manned mission to land on the Moon. It was launched on July 16, 1969, and Neil Armstrong became the first person to walk on the lunar surface on July 20, 1969. Buzz Aldrin joined him shortly after, while Michael Collins remained in lunar orbit.\"\nquestion = \"Who was the first person to walk on the Moon?\"\nmessages = [\n {\"role\": \"user\", \"content\": f\"Based on the following context, answer the question.\\n\\nContext: {context}\\n\\nQuestion: {question}\"}\n]\nINFERENCE_MODEL_NAME = f\"default--{OUTPUT_NAME}\"\nresponse = client.inference.gateway.provider.post(\n \"v1/chat/completions\",\n name=deployment_name,\n workspace=\"default\",\n body={\n \"model\": INFERENCE_MODEL_NAME,\n \"messages\": messages,\n \"temperature\": 0,\n \"max_tokens\": 256,\n }\n)\nprint(\"=\" * 60)\nprint(\"MODEL INFERENCE\")\nprint(\"=\" * 60)\nprint(f\"Question: {question}\")\nprint(f\"Expected: Neil Armstrong\")\nprint(f\"Model output: {response['choices'][0]['message']['content']}\")", "language": "python", - "source_html": "context = "The Apollo 11 mission was the first manned mission to land on the Moon. It was launched on July 16, 1969, and Neil Armstrong became the first person to walk on the lunar surface on July 20, 1969. Buzz Aldrin joined him shortly after, while Michael Collins remained in lunar orbit."\nquestion = "Who was the first person to walk on the Moon?"\nmessages = [\n {"role": "user", "content": f"Based on the following context, answer the question.\\n\\nContext: {context}\\n\\nQuestion: {question}"}\n]\nresponse = client.inference.gateway.provider.post(\n "v1/chat/completions",\n name=deployment_name,\n workspace="default",\n body={\n "model": OUTPUT_NAME,\n "messages": messages,\n "temperature": 0,\n "max_tokens": 256,\n }\n)\nprint("=" * 60)\nprint("MODEL INFERENCE")\nprint("=" * 60)\nprint(f"Question: {question}")\nprint(f"Expected: Neil Armstrong")\nprint(f"Model output: {response['choices'][0]['message']['content']}")\n" + "source_html": "context = "The Apollo 11 mission was the first manned mission to land on the Moon. It was launched on July 16, 1969, and Neil Armstrong became the first person to walk on the lunar surface on July 20, 1969. Buzz Aldrin joined him shortly after, while Michael Collins remained in lunar orbit."\nquestion = "Who was the first person to walk on the Moon?"\nmessages = [\n {"role": "user", "content": f"Based on the following context, answer the question.\\n\\nContext: {context}\\n\\nQuestion: {question}"}\n]\nINFERENCE_MODEL_NAME = f"default--{OUTPUT_NAME}"\nresponse = client.inference.gateway.provider.post(\n "v1/chat/completions",\n name=deployment_name,\n workspace="default",\n body={\n "model": INFERENCE_MODEL_NAME,\n "messages": messages,\n "temperature": 0,\n "max_tokens": 256,\n }\n)\nprint("=" * 60)\nprint("MODEL INFERENCE")\nprint("=" * 60)\nprint(f"Question: {question}")\nprint(f"Expected: Neil Armstrong")\nprint(f"Model output: {response['choices'][0]['message']['content']}")\n" }, { "type": "markdown", diff --git a/docs/fern/components/notebooks/optimize-throughput.json b/docs/fern/components/notebooks/optimize-throughput.json index caa20e705d..c4d4b90d72 100644 --- a/docs/fern/components/notebooks/optimize-throughput.json +++ b/docs/fern/components/notebooks/optimize-throughput.json @@ -7,8 +7,8 @@ }, { "type": "markdown", - "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)", - "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
\n" + "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **At least one GPU with CUDA 13+**", + "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. At least one GPU with CUDA 13+
  6. \n
\n" }, { "type": "markdown", @@ -56,25 +56,25 @@ }, { "type": "markdown", - "source": "### 3. Secrets Setup\n\nIf you plan to use NGC or HuggingFace models, you will need to configure authentication:\n\n- **NGC models** (`ngc://` URIs): Requires NGC API key\n- **HuggingFace models** (`hf://` URIs): Requires HF token for gated/private models\n\n\nConfigure these as secrets in your platform. Refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md) for detailed instructions.\n\nGet your credentials to access base models:\n- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n- [HuggingFace Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n\n\n---\n\n#### Quick Setup Example\n\nThis tutorial uses the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from HuggingFace. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access.\n\n**HuggingFace Authentication:**\n- For gated models (Llama, Gemma), you must provide a HuggingFace token via the `token_secret` parameter\n- Get your token from [HuggingFace Settings](https://huggingface.co/settings/tokens) (requires Read access)\n- Accept the model's terms on the HuggingFace model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main)\n- For public models, you can omit the `token_secret` parameter when creating a fileset for the model in the next step.", - "source_html": "

3. Secrets Setup

\n

If you plan to use NGC or HuggingFace models, you will need to configure authentication:

\n
    \n
  • NGC models (ngc:// URIs): Requires NGC API key
  • \n
  • HuggingFace models (hf:// URIs): Requires HF token for gated/private models
  • \n
\n

Configure these as secrets in your platform. Refer to Managing Secrets for detailed instructions.

\n

Get your credentials to access base models:

\n\n
\n

Quick Setup Example

\n

This tutorial uses the meta-llama/Llama-3.2-1B-Instruct model from HuggingFace. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the meta-llama/Llama-3.2-1B-Instruct Hugging Face page, request access.

\n

HuggingFace Authentication:

\n
    \n
  • For gated models (Llama, Gemma), you must provide a HuggingFace token via the token_secret parameter
  • \n
  • Get your token from HuggingFace Settings (requires Read access)
  • \n
  • Accept the model's terms on the HuggingFace model page before using it. Example: meta-llama/Llama-3.2-1B-Instruct
  • \n
  • For public models, you can omit the token_secret parameter when creating a fileset for the model in the next step.
  • \n
\n" + "source": "### 3. Secrets Setup\n\nIf you plan to use NGC or Hugging Face models, you will need to configure authentication:\n\n- **NGC models** (`ngc://` URIs): Requires NGC API key\n- **Hugging Face models** (`hf://` URIs): Requires HF token for gated/private models\n\n\nConfigure these as secrets in your platform. Refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md) for detailed instructions.\n\nGet your credentials to access base models:\n- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n- [Hugging Face Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n\n\n---\n\n#### Quick Setup Example\n\nThis tutorial uses the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from Hugging Face. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access.\n\n**Hugging Face Authentication:**\n- For gated models (Llama, Gemma), you must provide a Hugging Face token via the `token_secret` parameter\n- Get your token from [Hugging Face Settings](https://huggingface.co/settings/tokens) (requires Read access)\n- Accept the model's terms on the Hugging Face model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main)\n- For public models, you can omit the `token_secret` parameter when creating a fileset for the model in the next step.", + "source_html": "

3. Secrets Setup

\n

If you plan to use NGC or Hugging Face models, you will need to configure authentication:

\n
    \n
  • NGC models (ngc:// URIs): Requires NGC API key
  • \n
  • Hugging Face models (hf:// URIs): Requires HF token for gated/private models
  • \n
\n

Configure these as secrets in your platform. Refer to Managing Secrets for detailed instructions.

\n

Get your credentials to access base models:

\n\n
\n

Quick Setup Example

\n

This tutorial uses the meta-llama/Llama-3.2-1B-Instruct model from Hugging Face. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the meta-llama/Llama-3.2-1B-Instruct Hugging Face page, request access.

\n

Hugging Face Authentication:

\n
    \n
  • For gated models (Llama, Gemma), you must provide a Hugging Face token via the token_secret parameter
  • \n
  • Get your token from Hugging Face Settings (requires Read access)
  • \n
  • Accept the model's terms on the Hugging Face model page before using it. Example: meta-llama/Llama-3.2-1B-Instruct
  • \n
  • For public models, you can omit the token_secret parameter when creating a fileset for the model in the next step.
  • \n
\n" }, { "type": "code", - "source": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\nHF_TOKEN = os.getenv(\"HF_TOKEN\")\nNGC_API_KEY = os.getenv(\"NGC_API_KEY\")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f\"{label} environment variable is not set. Set it and try again.\")\n try:\n secret = client.secrets.create(\n name=name,\n workspace=\"default\",\n value=value,\n )\n print(f\"Created secret: {name}\")\n return secret\n except ConflictError:\n print(f\"Secret '{name}' already exists, continuing...\")\n return client.secrets.retrieve(name=name, workspace=\"default\")\n\n\n# Create HuggingFace token secret\nhf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\nprint(\"HF_TOKEN secret:\")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret(\"ngc-api-key\", NGC_API_KEY, \"NGC_API_KEY\")", + "source": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\nHF_TOKEN = os.getenv(\"HF_TOKEN\")\nNGC_API_KEY = os.getenv(\"NGC_API_KEY\")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f\"{label} environment variable is not set. Set it and try again.\")\n try:\n secret = client.secrets.create(\n name=name,\n workspace=\"default\",\n value=value,\n )\n print(f\"Created secret: {name}\")\n return secret\n except ConflictError:\n print(f\"Secret '{name}' already exists, continuing...\")\n return client.secrets.retrieve(name=name, workspace=\"default\")\n\n\n# Create Hugging Face token secret\nhf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\nprint(\"HF_TOKEN secret:\")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret(\"ngc-api-key\", NGC_API_KEY, \"NGC_API_KEY\")", "language": "python", - "source_html": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\nHF_TOKEN = os.getenv("HF_TOKEN")\nNGC_API_KEY = os.getenv("NGC_API_KEY")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f"{label} environment variable is not set. Set it and try again.")\n try:\n secret = client.secrets.create(\n name=name,\n workspace="default",\n value=value,\n )\n print(f"Created secret: {name}")\n return secret\n except ConflictError:\n print(f"Secret '{name}' already exists, continuing...")\n return client.secrets.retrieve(name=name, workspace="default")\n\n\n# Create HuggingFace token secret\nhf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN")\nprint("HF_TOKEN secret:")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret("ngc-api-key", NGC_API_KEY, "NGC_API_KEY")\n" + "source_html": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\nHF_TOKEN = os.getenv("HF_TOKEN")\nNGC_API_KEY = os.getenv("NGC_API_KEY")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f"{label} environment variable is not set. Set it and try again.")\n try:\n secret = client.secrets.create(\n name=name,\n workspace="default",\n value=value,\n )\n print(f"Created secret: {name}")\n return secret\n except ConflictError:\n print(f"Secret '{name}' already exists, continuing...")\n return client.secrets.retrieve(name=name, workspace="default")\n\n\n# Create Hugging Face token secret\nhf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN")\nprint("HF_TOKEN secret:")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret("ngc-api-key", NGC_API_KEY, "NGC_API_KEY")\n" }, { "type": "markdown", - "source": "### 4. Create Base Model FileSet\n\nCreate a fileset pointing to the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model on HuggingFace. This step creates a pointer to the model on Hugging Face and does not download it. The model is downloaded at job creation time.\n\nNote: for public models, you can omit the `token_secret` parameter when creating a model fileset.", - "source_html": "

4. Create Base Model FileSet

\n

Create a fileset pointing to the meta-llama/Llama-3.2-1B-Instruct model on HuggingFace. This step creates a pointer to the model on Hugging Face and does not download it. The model is downloaded at job creation time.

\n

Note: for public models, you can omit the token_secret parameter when creating a model fileset.

\n" + "source": "### 4. Create Base Model FileSet\n\nCreate a fileset pointing to the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model on Hugging Face. This step creates a pointer to the model on Hugging Face and does not download it. The model is downloaded at job creation time.\n\nNote: for public models, you can omit the `token_secret` parameter when creating a model fileset.", + "source_html": "

4. Create Base Model FileSet

\n

Create a fileset pointing to the meta-llama/Llama-3.2-1B-Instruct model on Hugging Face. This step creates a pointer to the model on Hugging Face and does not download it. The model is downloaded at job creation time.

\n

Note: for public models, you can omit the token_secret parameter when creating a model fileset.

\n" }, { "type": "code", - "source": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\nMODEL_NAME = \"llama-3-2-1b-base\"\n\n# Ensure you have a HuggingFace token secret created\n# Create a fileset pointing to the desired HuggingFace model\ntry:\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"Llama 3.2 1B base model from HuggingFace\",\n storage=HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\n print(f\"Created base model fileset: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model fileset already exists. Skipping creation.\")\n base_model_fs = client.files.filesets.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n print(f\"Created Model Entity: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model already exists. Updating fileset if different.\")\n base_model = client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n\nprint(f\"\\nBase model fileset: fileset://default/{base_model.name}\")\nprint(\"Base model fileset files list:\")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace=\"default\").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint(\"\\nWaiting for ModelSpec to be populated...\")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds\")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\nprint(f\"ModelSpec populated: {base_model.spec}\")", + "source": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\nMODEL_NAME = \"llama-3-2-1b-base\"\n\n# Ensure you have a Hugging Face token secret created\n# Create a fileset pointing to the desired Hugging Face model\ntry:\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"Llama 3.2 1B base model from Hugging Face\",\n storage=HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\n print(f\"Created base model fileset: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model fileset already exists. Skipping creation.\")\n base_model_fs = client.files.filesets.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n print(f\"Created Model Entity: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model already exists. Updating fileset if different.\")\n base_model = client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n\nprint(f\"\\nBase model fileset: fileset://default/{base_model.name}\")\nprint(\"Base model fileset files list:\")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace=\"default\").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint(\"\\nWaiting for ModelSpec to be populated...\")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds\")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\nprint(f\"ModelSpec populated: {base_model.spec}\")", "language": "python", - "source_html": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = "meta-llama/Llama-3.2-1B-Instruct"\nMODEL_NAME = "llama-3-2-1b-base"\n\n# Ensure you have a HuggingFace token secret created\n# Create a fileset pointing to the desired HuggingFace model\ntry:\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="Llama 3.2 1B base model from HuggingFace",\n storage=HuggingfaceStorageConfigParam(\n type="huggingface",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type="model",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\n print(f"Created base model fileset: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model fileset already exists. Skipping creation.")\n base_model_fs = client.files.filesets.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n print(f"Created Model Entity: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model already exists. Updating fileset if different.")\n base_model = client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n\nprint(f"\\nBase model fileset: fileset://default/{base_model.name}")\nprint("Base model fileset files list:")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace="default").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint("\\nWaiting for ModelSpec to be populated...")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\nprint(f"ModelSpec populated: {base_model.spec}")\n" + "source_html": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = "meta-llama/Llama-3.2-1B-Instruct"\nMODEL_NAME = "llama-3-2-1b-base"\n\n# Ensure you have a Hugging Face token secret created\n# Create a fileset pointing to the desired Hugging Face model\ntry:\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="Llama 3.2 1B base model from Hugging Face",\n storage=HuggingfaceStorageConfigParam(\n type="huggingface",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type="model",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\n print(f"Created base model fileset: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model fileset already exists. Skipping creation.")\n base_model_fs = client.files.filesets.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n print(f"Created Model Entity: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model already exists. Updating fileset if different.")\n base_model = client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n\nprint(f"\\nBase model fileset: fileset://default/{base_model.name}")\nprint("Base model fileset files list:")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace="default").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint("\\nWaiting for ModelSpec to be populated...")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\nprint(f"ModelSpec populated: {base_model.spec}")\n" }, { "type": "markdown", @@ -89,8 +89,8 @@ }, { "type": "markdown", - "source": "### 6. Track Finetuning Progress\n\nA training job contains multiple steps: \n- Model and dataset downloading\n- Finetuning where LoRA adapter weights are trained\n- Creating a fileset entry for the finetuned model\n- Finetuned weights uploading\n\nThe elapsed time printed below reflects progress of the entire job. We compare the time taken by the finetuning step for both jobs in the last section of this tutorial.", - "source_html": "

6. Track Finetuning Progress

\n

A training job contains multiple steps:

\n
    \n
  • Model and dataset downloading
  • \n
  • Finetuning where LoRA adapter weights are trained
  • \n
  • Creating a fileset entry for the finetuned model
  • \n
  • Finetuned weights uploading
  • \n
\n

The elapsed time printed below reflects progress of the entire job. We compare the time taken by the finetuning step for both jobs in the last section of this tutorial.

\n" + "source": "### 6. Track Fine-Tuning Progress\n\nA training job contains multiple steps: \n- Model and dataset downloading\n- Fine-tuning where LoRA adapter weights are trained\n- Creating a fileset entry for the fine-tuned model\n- Fine-tuned weights uploading\n\nThe elapsed time printed below reflects progress of the entire job. We compare the time taken by the fine-tuning step for both jobs in the last section of this tutorial.", + "source_html": "

6. Track Fine-Tuning Progress

\n

A training job contains multiple steps:

\n
    \n
  • Model and dataset downloading
  • \n
  • Fine-tuning where LoRA adapter weights are trained
  • \n
  • Creating a fileset entry for the fine-tuned model
  • \n
  • Fine-tuned weights uploading
  • \n
\n

The elapsed time printed below reflects progress of the entire job. We compare the time taken by the fine-tuning step for both jobs in the last section of this tutorial.

\n" }, { "type": "markdown", @@ -105,14 +105,14 @@ }, { "type": "markdown", - "source": "#### Monitor the Job Until Completion\n\nThe cell below polls the job status every 10 seconds and renders a live dashboard with validation loss, GPU VRAM usage, and GPU utilization charts. The charts appear empty at first while the model and dataset download; training metrics and GPU activity populate after the finetuning step begins.\n\n> **Note:** This is additional code. You can also use the Weights & Biases or MLflow integrations.", - "source_html": "

Monitor the Job Until Completion

\n

The cell below polls the job status every 10 seconds and renders a live dashboard with validation loss, GPU VRAM usage, and GPU utilization charts. The charts appear empty at first while the model and dataset download; training metrics and GPU activity populate after the finetuning step begins.

\n
\n

Note: This is additional code. You can also use the Weights & Biases or MLflow integrations.

\n
\n" + "source": "#### Monitor the Job Until Completion\n\nThe cell below polls the job status every 10 seconds and renders a live dashboard with validation loss, GPU VRAM usage, and GPU utilization charts. The charts appear empty at first while the model and dataset download; training metrics and GPU activity populate after the fine-tuning step begins.\n\n> **Note:** This is additional code. You can also use the Weights & Biases or MLflow integrations.", + "source_html": "

Monitor the Job Until Completion

\n

The cell below polls the job status every 10 seconds and renders a live dashboard with validation loss, GPU VRAM usage, and GPU utilization charts. The charts appear empty at first while the model and dataset download; training metrics and GPU activity populate after the fine-tuning step begins.

\n
\n

Note: This is additional code. You can also use the Weights & Biases or MLflow integrations.

\n
\n" }, { "type": "code", - "source": "import time\nfrom typing import cast\nfrom IPython.display import clear_output\nfrom nemo_platform.types.shared import PlatformJobStatusResponse\n\n# Timeout set to 30 minutes to accommodate typical LoRA training duration for this dataset size.\n# Actual training time will vary based on hardware, model size, and dataset complexity.\nTIMEOUT_SECONDS = 30 * 60 # 30 minutes\nVAL_LOSS_KEY = \"val_loss\"\nTRAIN_LOSS_KEY = \"loss\"\n\n# ---------------------------------------------------------------------------\n# Job polling with live dashboard\n# ---------------------------------------------------------------------------\n\ndef wait_for_job(\n workspace: str,\n job_name: str,\n timeout: int = TIMEOUT_SECONDS,\n poll_interval: int = 10,\n val_loss_key: str = VAL_LOSS_KEY,\n train_loss_key: str = TRAIN_LOSS_KEY,\n) -> PlatformJobStatusResponse:\n \"\"\"\n Poll job status until completed, failed, cancelled, or timeout.\n Displays a live dashboard with loss curves and GPU metrics.\n\n Args:\n workspace: The workspace where the job is running.\n job_name: The name of the job to monitor.\n timeout: Maximum time to wait in seconds (default: 30 minutes).\n poll_interval: Time between status checks in seconds (default: 10).\n\n Returns:\n The final job status response.\n \"\"\"\n start_time = time.time()\n\n # Time-series accumulators required for plotting\n elapsed_mins: list[float] = []\n val_losses: list[float | None] = []\n train_losses: list[float | None] = []\n vram_history: list[list[float]] = []\n util_history: list[list[float]] = []\n\n while True:\n elapsed = time.time() - start_time\n elapsed_min = elapsed / 60\n\n # Check for timeout\n if elapsed > timeout:\n error_message = f\"Timeout reached after {elapsed_min:.1f} minutes\"\n print(f\"\\n{error_message}\")\n print(\"Job did not complete within the timeout period.\")\n raise Exception(error_message)\n\n status = client.jobs.get_status(name=job_name, workspace=workspace)\n\n # -- Extract training progress from nested steps structure --\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n val_loss: float | None = None\n train_loss: float | None = None\n current_step_name: str | None = None\n current_step_phase: str | None = None\n\n for job_step in status.steps or []:\n # Track the current active step name and phase for progress display\n if job_step.tasks:\n task = job_step.tasks[0]\n td = task.status_details or {}\n phase = cast(str, td.get(\"phase\", \"\"))\n # Update current step if it's active or pending (not completed)\n if job_step.status in (\"active\", \"pending\"):\n current_step_name = job_step.name\n current_step_phase = phase or \"started\"\n\n if job_step.name == \"training\":\n for task in job_step.tasks or []:\n td = task.status_details or {}\n step = cast(int, td[\"step\"]) if \"step\" in td else None\n max_steps = cast(int, td[\"max_steps\"]) if \"max_steps\" in td else None\n training_phase = cast(str, td[\"phase\"]) if \"phase\" in td else None\n raw_val_loss = td.get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n raw_train_loss = td.get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n break\n break\n\n if val_loss is None:\n raw_val_loss = (status.status_details or {}).get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n if train_loss is None:\n raw_train_loss = (status.status_details or {}).get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n\n # -- Collect GPU snapshot --\n vram_pcts, util_pcts = _get_gpu_snapshot()\n\n # -- Append to accumulators used for the plots --\n elapsed_mins.append(elapsed_min)\n val_losses.append(val_loss)\n train_losses.append(train_loss)\n vram_history.append(vram_pcts)\n util_history.append(util_pcts)\n\n # -- Build status strings --\n status_str = f\"Status: {status.status}\"\n if step is not None and max_steps is not None:\n pct = step / max_steps * 100\n step_str = f\"Step {step}/{max_steps} ({pct:.0f}%)\"\n if training_phase:\n step_str += f\" - {training_phase}\"\n else:\n if current_step_name and current_step_phase:\n step_str = f\"{current_step_name} - {current_step_phase}\"\n elif current_step_name:\n step_str = f\"{current_step_name}\"\n else:\n step_str = \"Waiting for training to start...\"\n elapsed_str = f\"Elapsed: {elapsed_min:.1f} min\"\n\n # -- Redraw dashboard --\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n\n # -- Check terminal conditions --\n if status.status.lower() == \"completed\":\n # Redraw dashboard one final time with \"completed\" status\n status_str = f\"Status: {status.status}\"\n if step is not None and max_steps is not None:\n step_str = f\"Step {max_steps}/{max_steps} (100%)\"\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n print(f\"\\nJob completed in {elapsed_min:.1f} minutes ({elapsed:.0f}s)\")\n return status\n elif status.status.lower() in (\"failed\", \"cancelled\", \"error\"):\n print(f\"\\nJob finished with status: {status.status}\")\n print(f\"Total time elapsed: {elapsed_min:.1f} minutes ({elapsed:.0f}s)\")\n\n # Print error details from the job level\n if status.error_details:\n error_msg = status.error_details.get(\"message\", \"\")\n if error_msg:\n print(f\"\\nError: {error_msg}\")\n\n # Find and print error details from the failed step/task\n for job_step in status.steps or []:\n if job_step.status == \"error\":\n print(f\"\\nFailed step: {job_step.name}\")\n if job_step.error_details:\n step_error = job_step.error_details.get(\"message\", \"\")\n if step_error:\n print(f\"Step error: {step_error}\")\n # Get error_stack from the failed task\n for task in job_step.tasks or []:\n if task.status == \"error\" and hasattr(task, \"error_stack\") and task.error_stack:\n print(f\"\\nError stack trace:\\n{task.error_stack}\")\n elif task.status == \"error\" and task.error_details:\n task_error = task.error_details.get(\"message\", \"\")\n if task_error:\n print(f\"Task error: {task_error}\")\n break\n\n raise Exception(f\"Job finished with status: {status.status}\")\n\n time.sleep(poll_interval)\n\n\n# Wait for the job to complete\njob_with_sequence_packing_status = wait_for_job(\n workspace=\"default\",\n job_name=job_with_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS,\n)\n\npacked_val_loss = (job_with_sequence_packing_status.status_details or {}).get(\"val_loss\")\nif packed_val_loss is not None:\n print(f\"Validation loss: {float(packed_val_loss):.2f}\")\nelse:\n print(\"Validation loss: not reported in job status\")", + "source": "import time\nfrom typing import cast\nfrom IPython.display import clear_output\nfrom nemo_platform.types.shared import PlatformJobStatusResponse\n\n# Timeout set to 30 minutes to accommodate typical LoRA training duration for this dataset size.\n# Actual training time will vary based on hardware, model size, and dataset complexity.\nTIMEOUT_SECONDS = 30 * 60 # 30 minutes\nVAL_LOSS_KEY = \"val_loss\"\nTRAIN_LOSS_KEY = \"train_loss\"\n\n\ndef get_training_metric(\n status: PlatformJobStatusResponse,\n metric_key: str,\n) -> float | None:\n \"\"\"Return a metric reported by a task in the training step.\"\"\"\n for job_step in status.steps or []:\n if job_step.name == \"training\":\n for task in job_step.tasks or []:\n value = (task.status_details or {}).get(metric_key)\n if value is not None:\n return float(value)\n return None\n\n\n# ---------------------------------------------------------------------------\n# Job polling with live dashboard\n# ---------------------------------------------------------------------------\n\ndef wait_for_job(\n workspace: str,\n job_name: str,\n timeout: int = TIMEOUT_SECONDS,\n poll_interval: int = 10,\n val_loss_key: str = VAL_LOSS_KEY,\n train_loss_key: str = TRAIN_LOSS_KEY,\n) -> PlatformJobStatusResponse:\n \"\"\"\n Poll job status until completed, failed, cancelled, or timeout.\n Displays a live dashboard with loss curves and GPU metrics.\n\n Args:\n workspace: The workspace where the job is running.\n job_name: The name of the job to monitor.\n timeout: Maximum time to wait in seconds (default: 30 minutes).\n poll_interval: Time between status checks in seconds (default: 10).\n\n Returns:\n The final job status response.\n \"\"\"\n start_time = time.time()\n\n # Time-series accumulators required for plotting\n elapsed_mins: list[float] = []\n val_losses: list[float | None] = []\n train_losses: list[float | None] = []\n vram_history: list[list[float]] = []\n util_history: list[list[float]] = []\n\n while True:\n elapsed = time.time() - start_time\n elapsed_min = elapsed / 60\n\n # Check for timeout\n if elapsed > timeout:\n error_message = f\"Timeout reached after {elapsed_min:.1f} minutes\"\n print(f\"\\n{error_message}\")\n print(\"Job did not complete within the timeout period.\")\n raise Exception(error_message)\n\n status = client.jobs.get_status(name=job_name, workspace=workspace)\n\n # -- Extract training progress from nested steps structure --\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n val_loss: float | None = None\n train_loss: float | None = None\n current_step_name: str | None = None\n current_step_phase: str | None = None\n\n for job_step in status.steps or []:\n # Track the current active step name and phase for progress display\n if job_step.tasks:\n task = job_step.tasks[0]\n td = task.status_details or {}\n phase = cast(str, td.get(\"phase\", \"\"))\n # Update current step if it's active or pending (not completed)\n if job_step.status in (\"active\", \"pending\"):\n current_step_name = job_step.name\n current_step_phase = phase or \"started\"\n\n if job_step.name == \"training\":\n for task in job_step.tasks or []:\n td = task.status_details or {}\n step = cast(int, td[\"step\"]) if \"step\" in td else None\n max_steps = cast(int, td[\"max_steps\"]) if \"max_steps\" in td else None\n training_phase = cast(str, td[\"phase\"]) if \"phase\" in td else None\n raw_val_loss = td.get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n raw_train_loss = td.get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n break\n break\n\n if val_loss is None:\n raw_val_loss = (status.status_details or {}).get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n if train_loss is None:\n raw_train_loss = (status.status_details or {}).get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n\n # -- Collect GPU snapshot --\n vram_pcts, util_pcts = _get_gpu_snapshot()\n\n # -- Append to accumulators used for the plots --\n elapsed_mins.append(elapsed_min)\n val_losses.append(val_loss)\n train_losses.append(train_loss)\n vram_history.append(vram_pcts)\n util_history.append(util_pcts)\n\n # -- Build status strings --\n status_str = f\"Status: {status.status}\"\n if step is not None and max_steps is not None:\n pct = step / max_steps * 100\n step_str = f\"Step {step}/{max_steps} ({pct:.0f}%)\"\n if training_phase:\n step_str += f\" - {training_phase}\"\n else:\n if current_step_name and current_step_phase:\n step_str = f\"{current_step_name} - {current_step_phase}\"\n elif current_step_name:\n step_str = f\"{current_step_name}\"\n else:\n step_str = \"Waiting for training to start...\"\n elapsed_str = f\"Elapsed: {elapsed_min:.1f} min\"\n\n # -- Redraw dashboard --\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n\n # -- Check terminal conditions --\n if status.status.lower() == \"completed\":\n # Redraw dashboard one final time with \"completed\" status\n status_str = f\"Status: {status.status}\"\n if step is not None and max_steps is not None:\n step_str = f\"Step {max_steps}/{max_steps} (100%)\"\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n print(f\"\\nJob completed in {elapsed_min:.1f} minutes ({elapsed:.0f}s)\")\n return status\n elif status.status.lower() in (\"failed\", \"cancelled\", \"error\"):\n print(f\"\\nJob finished with status: {status.status}\")\n print(f\"Total time elapsed: {elapsed_min:.1f} minutes ({elapsed:.0f}s)\")\n\n # Print error details from the job level\n if status.error_details:\n error_msg = status.error_details.get(\"message\", \"\")\n if error_msg:\n print(f\"\\nError: {error_msg}\")\n\n # Find and print error details from the failed step/task\n for job_step in status.steps or []:\n if job_step.status == \"error\":\n print(f\"\\nFailed step: {job_step.name}\")\n if job_step.error_details:\n step_error = job_step.error_details.get(\"message\", \"\")\n if step_error:\n print(f\"Step error: {step_error}\")\n # Get error_stack from the failed task\n for task in job_step.tasks or []:\n if task.status == \"error\" and hasattr(task, \"error_stack\") and task.error_stack:\n print(f\"\\nError stack trace:\\n{task.error_stack}\")\n elif task.status == \"error\" and task.error_details:\n task_error = task.error_details.get(\"message\", \"\")\n if task_error:\n print(f\"Task error: {task_error}\")\n break\n\n raise Exception(f\"Job finished with status: {status.status}\")\n\n time.sleep(poll_interval)\n\n\n# Wait for the job to complete\njob_with_sequence_packing_status = wait_for_job(\n workspace=\"default\",\n job_name=job_with_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS,\n)\n\npacked_val_loss = get_training_metric(job_with_sequence_packing_status, VAL_LOSS_KEY)\nif packed_val_loss is not None:\n print(f\"Validation loss: {packed_val_loss:.2f}\")\nelse:\n print(\"Validation loss: not reported in job status\")", "language": "python", - "source_html": "import time\nfrom typing import cast\nfrom IPython.display import clear_output\nfrom nemo_platform.types.shared import PlatformJobStatusResponse\n\n# Timeout set to 30 minutes to accommodate typical LoRA training duration for this dataset size.\n# Actual training time will vary based on hardware, model size, and dataset complexity.\nTIMEOUT_SECONDS = 30 * 60 # 30 minutes\nVAL_LOSS_KEY = "val_loss"\nTRAIN_LOSS_KEY = "loss"\n\n# ---------------------------------------------------------------------------\n# Job polling with live dashboard\n# ---------------------------------------------------------------------------\n\ndef wait_for_job(\n workspace: str,\n job_name: str,\n timeout: int = TIMEOUT_SECONDS,\n poll_interval: int = 10,\n val_loss_key: str = VAL_LOSS_KEY,\n train_loss_key: str = TRAIN_LOSS_KEY,\n) -> PlatformJobStatusResponse:\n """\n Poll job status until completed, failed, cancelled, or timeout.\n Displays a live dashboard with loss curves and GPU metrics.\n\n Args:\n workspace: The workspace where the job is running.\n job_name: The name of the job to monitor.\n timeout: Maximum time to wait in seconds (default: 30 minutes).\n poll_interval: Time between status checks in seconds (default: 10).\n\n Returns:\n The final job status response.\n """\n start_time = time.time()\n\n # Time-series accumulators required for plotting\n elapsed_mins: list[float] = []\n val_losses: list[float | None] = []\n train_losses: list[float | None] = []\n vram_history: list[list[float]] = []\n util_history: list[list[float]] = []\n\n while True:\n elapsed = time.time() - start_time\n elapsed_min = elapsed / 60\n\n # Check for timeout\n if elapsed > timeout:\n error_message = f"Timeout reached after {elapsed_min:.1f} minutes"\n print(f"\\n{error_message}")\n print("Job did not complete within the timeout period.")\n raise Exception(error_message)\n\n status = client.jobs.get_status(name=job_name, workspace=workspace)\n\n # -- Extract training progress from nested steps structure --\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n val_loss: float | None = None\n train_loss: float | None = None\n current_step_name: str | None = None\n current_step_phase: str | None = None\n\n for job_step in status.steps or []:\n # Track the current active step name and phase for progress display\n if job_step.tasks:\n task = job_step.tasks[0]\n td = task.status_details or {}\n phase = cast(str, td.get("phase", ""))\n # Update current step if it's active or pending (not completed)\n if job_step.status in ("active", "pending"):\n current_step_name = job_step.name\n current_step_phase = phase or "started"\n\n if job_step.name == "training":\n for task in job_step.tasks or []:\n td = task.status_details or {}\n step = cast(int, td["step"]) if "step" in td else None\n max_steps = cast(int, td["max_steps"]) if "max_steps" in td else None\n training_phase = cast(str, td["phase"]) if "phase" in td else None\n raw_val_loss = td.get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n raw_train_loss = td.get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n break\n break\n\n if val_loss is None:\n raw_val_loss = (status.status_details or {}).get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n if train_loss is None:\n raw_train_loss = (status.status_details or {}).get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n\n # -- Collect GPU snapshot --\n vram_pcts, util_pcts = _get_gpu_snapshot()\n\n # -- Append to accumulators used for the plots --\n elapsed_mins.append(elapsed_min)\n val_losses.append(val_loss)\n train_losses.append(train_loss)\n vram_history.append(vram_pcts)\n util_history.append(util_pcts)\n\n # -- Build status strings --\n status_str = f"Status: {status.status}"\n if step is not None and max_steps is not None:\n pct = step / max_steps * 100\n step_str = f"Step {step}/{max_steps} ({pct:.0f}%)"\n if training_phase:\n step_str += f" - {training_phase}"\n else:\n if current_step_name and current_step_phase:\n step_str = f"{current_step_name} - {current_step_phase}"\n elif current_step_name:\n step_str = f"{current_step_name}"\n else:\n step_str = "Waiting for training to start..."\n elapsed_str = f"Elapsed: {elapsed_min:.1f} min"\n\n # -- Redraw dashboard --\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n\n # -- Check terminal conditions --\n if status.status.lower() == "completed":\n # Redraw dashboard one final time with "completed" status\n status_str = f"Status: {status.status}"\n if step is not None and max_steps is not None:\n step_str = f"Step {max_steps}/{max_steps} (100%)"\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n print(f"\\nJob completed in {elapsed_min:.1f} minutes ({elapsed:.0f}s)")\n return status\n elif status.status.lower() in ("failed", "cancelled", "error"):\n print(f"\\nJob finished with status: {status.status}")\n print(f"Total time elapsed: {elapsed_min:.1f} minutes ({elapsed:.0f}s)")\n\n # Print error details from the job level\n if status.error_details:\n error_msg = status.error_details.get("message", "")\n if error_msg:\n print(f"\\nError: {error_msg}")\n\n # Find and print error details from the failed step/task\n for job_step in status.steps or []:\n if job_step.status == "error":\n print(f"\\nFailed step: {job_step.name}")\n if job_step.error_details:\n step_error = job_step.error_details.get("message", "")\n if step_error:\n print(f"Step error: {step_error}")\n # Get error_stack from the failed task\n for task in job_step.tasks or []:\n if task.status == "error" and hasattr(task, "error_stack") and task.error_stack:\n print(f"\\nError stack trace:\\n{task.error_stack}")\n elif task.status == "error" and task.error_details:\n task_error = task.error_details.get("message", "")\n if task_error:\n print(f"Task error: {task_error}")\n break\n\n raise Exception(f"Job finished with status: {status.status}")\n\n time.sleep(poll_interval)\n\n\n# Wait for the job to complete\njob_with_sequence_packing_status = wait_for_job(\n workspace="default",\n job_name=job_with_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS,\n)\n\npacked_val_loss = (job_with_sequence_packing_status.status_details or {}).get("val_loss")\nif packed_val_loss is not None:\n print(f"Validation loss: {float(packed_val_loss):.2f}")\nelse:\n print("Validation loss: not reported in job status")\n" + "source_html": "import time\nfrom typing import cast\nfrom IPython.display import clear_output\nfrom nemo_platform.types.shared import PlatformJobStatusResponse\n\n# Timeout set to 30 minutes to accommodate typical LoRA training duration for this dataset size.\n# Actual training time will vary based on hardware, model size, and dataset complexity.\nTIMEOUT_SECONDS = 30 * 60 # 30 minutes\nVAL_LOSS_KEY = "val_loss"\nTRAIN_LOSS_KEY = "train_loss"\n\n\ndef get_training_metric(\n status: PlatformJobStatusResponse,\n metric_key: str,\n) -> float | None:\n """Return a metric reported by a task in the training step."""\n for job_step in status.steps or []:\n if job_step.name == "training":\n for task in job_step.tasks or []:\n value = (task.status_details or {}).get(metric_key)\n if value is not None:\n return float(value)\n return None\n\n\n# ---------------------------------------------------------------------------\n# Job polling with live dashboard\n# ---------------------------------------------------------------------------\n\ndef wait_for_job(\n workspace: str,\n job_name: str,\n timeout: int = TIMEOUT_SECONDS,\n poll_interval: int = 10,\n val_loss_key: str = VAL_LOSS_KEY,\n train_loss_key: str = TRAIN_LOSS_KEY,\n) -> PlatformJobStatusResponse:\n """\n Poll job status until completed, failed, cancelled, or timeout.\n Displays a live dashboard with loss curves and GPU metrics.\n\n Args:\n workspace: The workspace where the job is running.\n job_name: The name of the job to monitor.\n timeout: Maximum time to wait in seconds (default: 30 minutes).\n poll_interval: Time between status checks in seconds (default: 10).\n\n Returns:\n The final job status response.\n """\n start_time = time.time()\n\n # Time-series accumulators required for plotting\n elapsed_mins: list[float] = []\n val_losses: list[float | None] = []\n train_losses: list[float | None] = []\n vram_history: list[list[float]] = []\n util_history: list[list[float]] = []\n\n while True:\n elapsed = time.time() - start_time\n elapsed_min = elapsed / 60\n\n # Check for timeout\n if elapsed > timeout:\n error_message = f"Timeout reached after {elapsed_min:.1f} minutes"\n print(f"\\n{error_message}")\n print("Job did not complete within the timeout period.")\n raise Exception(error_message)\n\n status = client.jobs.get_status(name=job_name, workspace=workspace)\n\n # -- Extract training progress from nested steps structure --\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n val_loss: float | None = None\n train_loss: float | None = None\n current_step_name: str | None = None\n current_step_phase: str | None = None\n\n for job_step in status.steps or []:\n # Track the current active step name and phase for progress display\n if job_step.tasks:\n task = job_step.tasks[0]\n td = task.status_details or {}\n phase = cast(str, td.get("phase", ""))\n # Update current step if it's active or pending (not completed)\n if job_step.status in ("active", "pending"):\n current_step_name = job_step.name\n current_step_phase = phase or "started"\n\n if job_step.name == "training":\n for task in job_step.tasks or []:\n td = task.status_details or {}\n step = cast(int, td["step"]) if "step" in td else None\n max_steps = cast(int, td["max_steps"]) if "max_steps" in td else None\n training_phase = cast(str, td["phase"]) if "phase" in td else None\n raw_val_loss = td.get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n raw_train_loss = td.get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n break\n break\n\n if val_loss is None:\n raw_val_loss = (status.status_details or {}).get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n if train_loss is None:\n raw_train_loss = (status.status_details or {}).get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n\n # -- Collect GPU snapshot --\n vram_pcts, util_pcts = _get_gpu_snapshot()\n\n # -- Append to accumulators used for the plots --\n elapsed_mins.append(elapsed_min)\n val_losses.append(val_loss)\n train_losses.append(train_loss)\n vram_history.append(vram_pcts)\n util_history.append(util_pcts)\n\n # -- Build status strings --\n status_str = f"Status: {status.status}"\n if step is not None and max_steps is not None:\n pct = step / max_steps * 100\n step_str = f"Step {step}/{max_steps} ({pct:.0f}%)"\n if training_phase:\n step_str += f" - {training_phase}"\n else:\n if current_step_name and current_step_phase:\n step_str = f"{current_step_name} - {current_step_phase}"\n elif current_step_name:\n step_str = f"{current_step_name}"\n else:\n step_str = "Waiting for training to start..."\n elapsed_str = f"Elapsed: {elapsed_min:.1f} min"\n\n # -- Redraw dashboard --\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n\n # -- Check terminal conditions --\n if status.status.lower() == "completed":\n # Redraw dashboard one final time with "completed" status\n status_str = f"Status: {status.status}"\n if step is not None and max_steps is not None:\n step_str = f"Step {max_steps}/{max_steps} (100%)"\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n print(f"\\nJob completed in {elapsed_min:.1f} minutes ({elapsed:.0f}s)")\n return status\n elif status.status.lower() in ("failed", "cancelled", "error"):\n print(f"\\nJob finished with status: {status.status}")\n print(f"Total time elapsed: {elapsed_min:.1f} minutes ({elapsed:.0f}s)")\n\n # Print error details from the job level\n if status.error_details:\n error_msg = status.error_details.get("message", "")\n if error_msg:\n print(f"\\nError: {error_msg}")\n\n # Find and print error details from the failed step/task\n for job_step in status.steps or []:\n if job_step.status == "error":\n print(f"\\nFailed step: {job_step.name}")\n if job_step.error_details:\n step_error = job_step.error_details.get("message", "")\n if step_error:\n print(f"Step error: {step_error}")\n # Get error_stack from the failed task\n for task in job_step.tasks or []:\n if task.status == "error" and hasattr(task, "error_stack") and task.error_stack:\n print(f"\\nError stack trace:\\n{task.error_stack}")\n elif task.status == "error" and task.error_details:\n task_error = task.error_details.get("message", "")\n if task_error:\n print(f"Task error: {task_error}")\n break\n\n raise Exception(f"Job finished with status: {status.status}")\n\n time.sleep(poll_interval)\n\n\n# Wait for the job to complete\njob_with_sequence_packing_status = wait_for_job(\n workspace="default",\n job_name=job_with_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS,\n)\n\npacked_val_loss = get_training_metric(job_with_sequence_packing_status, VAL_LOSS_KEY)\nif packed_val_loss is not None:\n print(f"Validation loss: {packed_val_loss:.2f}")\nelse:\n print("Validation loss: not reported in job status")\n" }, { "type": "markdown", @@ -127,14 +127,14 @@ }, { "type": "markdown", - "source": "### 8. Track Finetuning Progress for Job without Sequence Packing", - "source_html": "

8. Track Finetuning Progress for Job without Sequence Packing

\n" + "source": "### 8. Track Fine-Tuning Progress for Job without Sequence Packing", + "source_html": "

8. Track Fine-Tuning Progress for Job without Sequence Packing

\n" }, { "type": "code", - "source": "# Wait for the training step to complete\njob_without_sequence_packing_status = wait_for_job(\n workspace=\"default\",\n job_name=job_without_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS\n)\n\nno_pack_val_loss = (job_without_sequence_packing_status.status_details or {}).get(\"val_loss\")\nif no_pack_val_loss is not None:\n print(f\"Validation loss: {float(no_pack_val_loss):.2f}\")\nelse:\n print(\"Validation loss: not reported in job status\")", + "source": "# Wait for the training step to complete\njob_without_sequence_packing_status = wait_for_job(\n workspace=\"default\",\n job_name=job_without_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS\n)\n\nno_pack_val_loss = get_training_metric(job_without_sequence_packing_status, VAL_LOSS_KEY)\nif no_pack_val_loss is not None:\n print(f\"Validation loss: {no_pack_val_loss:.2f}\")\nelse:\n print(\"Validation loss: not reported in job status\")", "language": "python", - "source_html": "# Wait for the training step to complete\njob_without_sequence_packing_status = wait_for_job(\n workspace="default",\n job_name=job_without_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS\n)\n\nno_pack_val_loss = (job_without_sequence_packing_status.status_details or {}).get("val_loss")\nif no_pack_val_loss is not None:\n print(f"Validation loss: {float(no_pack_val_loss):.2f}")\nelse:\n print("Validation loss: not reported in job status")\n" + "source_html": "# Wait for the training step to complete\njob_without_sequence_packing_status = wait_for_job(\n workspace="default",\n job_name=job_without_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS\n)\n\nno_pack_val_loss = get_training_metric(job_without_sequence_packing_status, VAL_LOSS_KEY)\nif no_pack_val_loss is not None:\n print(f"Validation loss: {no_pack_val_loss:.2f}")\nelse:\n print("Validation loss: not reported in job status")\n" }, { "type": "markdown", @@ -143,14 +143,14 @@ }, { "type": "code", - "source": "from nemo_platform.types.jobs import PlatformJobStep\nfrom datetime import datetime\nimport pandas as pd\n\nSTEP_NAME = \"training\"\n\ndef get_elapsed_time(step: PlatformJobStep) -> float:\n \"\"\"Calculate elapsed time in seconds from step's created_at to updated_at.\"\"\"\n created_at = datetime.fromisoformat(step.created_at.replace(\"Z\", \"+00:00\"))\n updated_at = datetime.fromisoformat(step.updated_at.replace(\"Z\", \"+00:00\"))\n return (updated_at - created_at).total_seconds()\n\nstep_with_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace=\"default\",\n job=job_with_sequence_packing.job.name,\n)\n\nstep_without_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace=\"default\",\n job=job_without_sequence_packing.job.name,\n)\n\ntime_to_complete_with_sequence_packing = get_elapsed_time(step_with_sequence_packing)\ntime_to_complete_without_sequence_packing = get_elapsed_time(step_without_sequence_packing)\n\n# Display results as a table\nresults_df = pd.DataFrame({\n \"Seq Packing Enabled\": [True, False],\n \"Val Loss\": [\n (job_with_sequence_packing_status.status_details or {}).get(\"val_loss\"),\n (job_without_sequence_packing_status.status_details or {}).get(\"val_loss\"),\n ],\n \"Training Step Time, sec\": [\n time_to_complete_with_sequence_packing,\n time_to_complete_without_sequence_packing\n ]\n})\n\nresults_df.style.format({\"Val Loss\": \"{:.2f}\", \"Training Step Time, sec\": \"{:.0f}\"}).hide(axis='index')", + "source": "from nemo_platform.types.jobs import PlatformJobStep\nimport pandas as pd\n\nSTEP_NAME = \"training\"\n\ndef get_elapsed_time(step: PlatformJobStep) -> float:\n \"\"\"Calculate elapsed time in seconds from step's created_at to updated_at.\"\"\"\n if step.created_at is None or step.updated_at is None:\n raise ValueError(\"Training step timestamps are unavailable\")\n return (step.updated_at - step.created_at).total_seconds()\n\nstep_with_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace=\"default\",\n job=job_with_sequence_packing.job.name,\n)\n\nstep_without_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace=\"default\",\n job=job_without_sequence_packing.job.name,\n)\n\ntime_to_complete_with_sequence_packing = get_elapsed_time(step_with_sequence_packing)\ntime_to_complete_without_sequence_packing = get_elapsed_time(step_without_sequence_packing)\n\n# Display results as a table\nresults_df = pd.DataFrame({\n \"Seq Packing Enabled\": [True, False],\n \"Val Loss\": [packed_val_loss, no_pack_val_loss],\n \"Training Step Time, sec\": [\n time_to_complete_with_sequence_packing,\n time_to_complete_without_sequence_packing\n ]\n})\n\nresults_df.style.format({\"Val Loss\": \"{:.2f}\", \"Training Step Time, sec\": \"{:.0f}\"}).hide(axis='index')", "language": "python", - "source_html": "from nemo_platform.types.jobs import PlatformJobStep\nfrom datetime import datetime\nimport pandas as pd\n\nSTEP_NAME = "training"\n\ndef get_elapsed_time(step: PlatformJobStep) -> float:\n """Calculate elapsed time in seconds from step's created_at to updated_at."""\n created_at = datetime.fromisoformat(step.created_at.replace("Z", "+00:00"))\n updated_at = datetime.fromisoformat(step.updated_at.replace("Z", "+00:00"))\n return (updated_at - created_at).total_seconds()\n\nstep_with_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace="default",\n job=job_with_sequence_packing.job.name,\n)\n\nstep_without_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace="default",\n job=job_without_sequence_packing.job.name,\n)\n\ntime_to_complete_with_sequence_packing = get_elapsed_time(step_with_sequence_packing)\ntime_to_complete_without_sequence_packing = get_elapsed_time(step_without_sequence_packing)\n\n# Display results as a table\nresults_df = pd.DataFrame({\n "Seq Packing Enabled": [True, False],\n "Val Loss": [\n (job_with_sequence_packing_status.status_details or {}).get("val_loss"),\n (job_without_sequence_packing_status.status_details or {}).get("val_loss"),\n ],\n "Training Step Time, sec": [\n time_to_complete_with_sequence_packing,\n time_to_complete_without_sequence_packing\n ]\n})\n\nresults_df.style.format({"Val Loss": "{:.2f}", "Training Step Time, sec": "{:.0f}"}).hide(axis='index')\n" + "source_html": "from nemo_platform.types.jobs import PlatformJobStep\nimport pandas as pd\n\nSTEP_NAME = "training"\n\ndef get_elapsed_time(step: PlatformJobStep) -> float:\n """Calculate elapsed time in seconds from step's created_at to updated_at."""\n if step.created_at is None or step.updated_at is None:\n raise ValueError("Training step timestamps are unavailable")\n return (step.updated_at - step.created_at).total_seconds()\n\nstep_with_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace="default",\n job=job_with_sequence_packing.job.name,\n)\n\nstep_without_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace="default",\n job=job_without_sequence_packing.job.name,\n)\n\ntime_to_complete_with_sequence_packing = get_elapsed_time(step_with_sequence_packing)\ntime_to_complete_without_sequence_packing = get_elapsed_time(step_without_sequence_packing)\n\n# Display results as a table\nresults_df = pd.DataFrame({\n "Seq Packing Enabled": [True, False],\n "Val Loss": [packed_val_loss, no_pack_val_loss],\n "Training Step Time, sec": [\n time_to_complete_with_sequence_packing,\n time_to_complete_without_sequence_packing\n ]\n})\n\nresults_df.style.format({"Val Loss": "{:.2f}", "Training Step Time, sec": "{:.0f}"}).hide(axis='index')\n" }, { "type": "markdown", - "source": "#### Examples of Validation Loss\n\nThe expected validation loss curves should match closely for both jobs.\n![Validation loss comparison chart showing similar convergence patterns between sequence-packed and non-packed training runs over training steps](../_images/packed_vs_not_packed_val_loss.png)\n\nSequence packed version should complete significantly faster.\n![Runtime comparison chart demonstrating significantly reduced training time for sequence-packed job compared to non-packed baseline](../_images/runtime.png)\n\n#### GPU Utilization\nSequence packed version should have a higher GPU utilization.\n![GPU utilization chart showing higher and more consistent GPU usage with sequence packing enabled throughout the training process](../_images/gpu_utilization.png)\n\n#### GPU Memory Allocation\nSequence packed version should have a higher GPU Memory Allocation.\n![GPU memory allocation chart illustrating increased memory utilization efficiency with sequence packing enabled](../_images/gpu_memory.png)", - "source_html": "

Examples of Validation Loss

\n

The expected validation loss curves should match closely for both jobs.\n\"Validation

\n

Sequence packed version should complete significantly faster.\n\"Runtime

\n

GPU Utilization

\n

Sequence packed version should have a higher GPU utilization.\n\"GPU

\n

GPU Memory Allocation

\n

Sequence packed version should have a higher GPU Memory Allocation.\n\"GPU

\n" + "source": "#### Examples of Validation Loss\n\nThe expected validation loss curves should match closely for both jobs.\n![Validation loss comparison chart showing similar convergence patterns between sequence-packed and non-packed training runs over training steps](../_images/packed_vs_not_packed_val_loss.png)\n\nSequence packed version should complete significantly faster.\n![Runtime comparison chart demonstrating significantly reduced training time for sequence-packed job compared to non-packed baseline](../_images/runtime.png)\n\n#### GPU Utilization\nSequence packed version should have a higher GPU utilization.\n![GPU utilization chart showing higher and more consistent GPU usage with sequence packing enabled throughout the training process](../_images/gpu_utilization.png)\n\n#### GPU Memory Allocation\nSequence packed version should have a higher GPU Memory Allocation.\n![GPU memory allocation chart illustrating increased memory utilization efficiency with sequence packing enabled](../_images/gpu_memory.png)\n\n## Next Steps\n\n- [Monitor customization metrics](fine-tune-metrics) for training and validation loss.\n- [Create a LoRA customization job](./lora-customization-job).\n- [Create a Full SFT customization job](./sft-customization-job).", + "source_html": "

Examples of Validation Loss

\n

The expected validation loss curves should match closely for both jobs.\n\"Validation

\n

Sequence packed version should complete significantly faster.\n\"Runtime

\n

GPU Utilization

\n

Sequence packed version should have a higher GPU utilization.\n\"GPU

\n

GPU Memory Allocation

\n

Sequence packed version should have a higher GPU Memory Allocation.\n\"GPU

\n

Next Steps

\n\n" } ] } \ No newline at end of file diff --git a/docs/fern/components/notebooks/optimize-throughput.ts b/docs/fern/components/notebooks/optimize-throughput.ts index 8fab564b50..f97784a78e 100644 --- a/docs/fern/components/notebooks/optimize-throughput.ts +++ b/docs/fern/components/notebooks/optimize-throughput.ts @@ -12,8 +12,8 @@ export default { cells: [ }, { "type": "markdown", - "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)", - "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
\n" + "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **At least one GPU with CUDA 13+**", + "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. At least one GPU with CUDA 13+
  6. \n
\n" }, { "type": "markdown", @@ -61,25 +61,25 @@ export default { cells: [ }, { "type": "markdown", - "source": "### 3. Secrets Setup\n\nIf you plan to use NGC or HuggingFace models, you will need to configure authentication:\n\n- **NGC models** (`ngc://` URIs): Requires NGC API key\n- **HuggingFace models** (`hf://` URIs): Requires HF token for gated/private models\n\n\nConfigure these as secrets in your platform. Refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md) for detailed instructions.\n\nGet your credentials to access base models:\n- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n- [HuggingFace Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n\n\n---\n\n#### Quick Setup Example\n\nThis tutorial uses the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from HuggingFace. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access.\n\n**HuggingFace Authentication:**\n- For gated models (Llama, Gemma), you must provide a HuggingFace token via the `token_secret` parameter\n- Get your token from [HuggingFace Settings](https://huggingface.co/settings/tokens) (requires Read access)\n- Accept the model's terms on the HuggingFace model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main)\n- For public models, you can omit the `token_secret` parameter when creating a fileset for the model in the next step.", - "source_html": "

3. Secrets Setup

\n

If you plan to use NGC or HuggingFace models, you will need to configure authentication:

\n
    \n
  • NGC models (ngc:// URIs): Requires NGC API key
  • \n
  • HuggingFace models (hf:// URIs): Requires HF token for gated/private models
  • \n
\n

Configure these as secrets in your platform. Refer to Managing Secrets for detailed instructions.

\n

Get your credentials to access base models:

\n\n
\n

Quick Setup Example

\n

This tutorial uses the meta-llama/Llama-3.2-1B-Instruct model from HuggingFace. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the meta-llama/Llama-3.2-1B-Instruct Hugging Face page, request access.

\n

HuggingFace Authentication:

\n
    \n
  • For gated models (Llama, Gemma), you must provide a HuggingFace token via the token_secret parameter
  • \n
  • Get your token from HuggingFace Settings (requires Read access)
  • \n
  • Accept the model's terms on the HuggingFace model page before using it. Example: meta-llama/Llama-3.2-1B-Instruct
  • \n
  • For public models, you can omit the token_secret parameter when creating a fileset for the model in the next step.
  • \n
\n" + "source": "### 3. Secrets Setup\n\nIf you plan to use NGC or Hugging Face models, you will need to configure authentication:\n\n- **NGC models** (`ngc://` URIs): Requires NGC API key\n- **Hugging Face models** (`hf://` URIs): Requires HF token for gated/private models\n\n\nConfigure these as secrets in your platform. Refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md) for detailed instructions.\n\nGet your credentials to access base models:\n- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n- [Hugging Face Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n\n\n---\n\n#### Quick Setup Example\n\nThis tutorial uses the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from Hugging Face. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access.\n\n**Hugging Face Authentication:**\n- For gated models (Llama, Gemma), you must provide a Hugging Face token via the `token_secret` parameter\n- Get your token from [Hugging Face Settings](https://huggingface.co/settings/tokens) (requires Read access)\n- Accept the model's terms on the Hugging Face model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main)\n- For public models, you can omit the `token_secret` parameter when creating a fileset for the model in the next step.", + "source_html": "

3. Secrets Setup

\n

If you plan to use NGC or Hugging Face models, you will need to configure authentication:

\n
    \n
  • NGC models (ngc:// URIs): Requires NGC API key
  • \n
  • Hugging Face models (hf:// URIs): Requires HF token for gated/private models
  • \n
\n

Configure these as secrets in your platform. Refer to Managing Secrets for detailed instructions.

\n

Get your credentials to access base models:

\n\n
\n

Quick Setup Example

\n

This tutorial uses the meta-llama/Llama-3.2-1B-Instruct model from Hugging Face. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the meta-llama/Llama-3.2-1B-Instruct Hugging Face page, request access.

\n

Hugging Face Authentication:

\n
    \n
  • For gated models (Llama, Gemma), you must provide a Hugging Face token via the token_secret parameter
  • \n
  • Get your token from Hugging Face Settings (requires Read access)
  • \n
  • Accept the model's terms on the Hugging Face model page before using it. Example: meta-llama/Llama-3.2-1B-Instruct
  • \n
  • For public models, you can omit the token_secret parameter when creating a fileset for the model in the next step.
  • \n
\n" }, { "type": "code", - "source": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\nHF_TOKEN = os.getenv(\"HF_TOKEN\")\nNGC_API_KEY = os.getenv(\"NGC_API_KEY\")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f\"{label} environment variable is not set. Set it and try again.\")\n try:\n secret = client.secrets.create(\n name=name,\n workspace=\"default\",\n value=value,\n )\n print(f\"Created secret: {name}\")\n return secret\n except ConflictError:\n print(f\"Secret '{name}' already exists, continuing...\")\n return client.secrets.retrieve(name=name, workspace=\"default\")\n\n\n# Create HuggingFace token secret\nhf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\nprint(\"HF_TOKEN secret:\")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret(\"ngc-api-key\", NGC_API_KEY, \"NGC_API_KEY\")", + "source": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\nHF_TOKEN = os.getenv(\"HF_TOKEN\")\nNGC_API_KEY = os.getenv(\"NGC_API_KEY\")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f\"{label} environment variable is not set. Set it and try again.\")\n try:\n secret = client.secrets.create(\n name=name,\n workspace=\"default\",\n value=value,\n )\n print(f\"Created secret: {name}\")\n return secret\n except ConflictError:\n print(f\"Secret '{name}' already exists, continuing...\")\n return client.secrets.retrieve(name=name, workspace=\"default\")\n\n\n# Create Hugging Face token secret\nhf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\nprint(\"HF_TOKEN secret:\")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret(\"ngc-api-key\", NGC_API_KEY, \"NGC_API_KEY\")", "language": "python", - "source_html": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\nHF_TOKEN = os.getenv("HF_TOKEN")\nNGC_API_KEY = os.getenv("NGC_API_KEY")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f"{label} environment variable is not set. Set it and try again.")\n try:\n secret = client.secrets.create(\n name=name,\n workspace="default",\n value=value,\n )\n print(f"Created secret: {name}")\n return secret\n except ConflictError:\n print(f"Secret '{name}' already exists, continuing...")\n return client.secrets.retrieve(name=name, workspace="default")\n\n\n# Create HuggingFace token secret\nhf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN")\nprint("HF_TOKEN secret:")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret("ngc-api-key", NGC_API_KEY, "NGC_API_KEY")\n" + "source_html": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\nHF_TOKEN = os.getenv("HF_TOKEN")\nNGC_API_KEY = os.getenv("NGC_API_KEY")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f"{label} environment variable is not set. Set it and try again.")\n try:\n secret = client.secrets.create(\n name=name,\n workspace="default",\n value=value,\n )\n print(f"Created secret: {name}")\n return secret\n except ConflictError:\n print(f"Secret '{name}' already exists, continuing...")\n return client.secrets.retrieve(name=name, workspace="default")\n\n\n# Create Hugging Face token secret\nhf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN")\nprint("HF_TOKEN secret:")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret("ngc-api-key", NGC_API_KEY, "NGC_API_KEY")\n" }, { "type": "markdown", - "source": "### 4. Create Base Model FileSet\n\nCreate a fileset pointing to the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model on HuggingFace. This step creates a pointer to the model on Hugging Face and does not download it. The model is downloaded at job creation time.\n\nNote: for public models, you can omit the `token_secret` parameter when creating a model fileset.", - "source_html": "

4. Create Base Model FileSet

\n

Create a fileset pointing to the meta-llama/Llama-3.2-1B-Instruct model on HuggingFace. This step creates a pointer to the model on Hugging Face and does not download it. The model is downloaded at job creation time.

\n

Note: for public models, you can omit the token_secret parameter when creating a model fileset.

\n" + "source": "### 4. Create Base Model FileSet\n\nCreate a fileset pointing to the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model on Hugging Face. This step creates a pointer to the model on Hugging Face and does not download it. The model is downloaded at job creation time.\n\nNote: for public models, you can omit the `token_secret` parameter when creating a model fileset.", + "source_html": "

4. Create Base Model FileSet

\n

Create a fileset pointing to the meta-llama/Llama-3.2-1B-Instruct model on Hugging Face. This step creates a pointer to the model on Hugging Face and does not download it. The model is downloaded at job creation time.

\n

Note: for public models, you can omit the token_secret parameter when creating a model fileset.

\n" }, { "type": "code", - "source": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\nMODEL_NAME = \"llama-3-2-1b-base\"\n\n# Ensure you have a HuggingFace token secret created\n# Create a fileset pointing to the desired HuggingFace model\ntry:\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"Llama 3.2 1B base model from HuggingFace\",\n storage=HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\n print(f\"Created base model fileset: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model fileset already exists. Skipping creation.\")\n base_model_fs = client.files.filesets.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n print(f\"Created Model Entity: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model already exists. Updating fileset if different.\")\n base_model = client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n\nprint(f\"\\nBase model fileset: fileset://default/{base_model.name}\")\nprint(\"Base model fileset files list:\")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace=\"default\").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint(\"\\nWaiting for ModelSpec to be populated...\")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds\")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\nprint(f\"ModelSpec populated: {base_model.spec}\")", + "source": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\nMODEL_NAME = \"llama-3-2-1b-base\"\n\n# Ensure you have a Hugging Face token secret created\n# Create a fileset pointing to the desired Hugging Face model\ntry:\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"Llama 3.2 1B base model from Hugging Face\",\n storage=HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\n print(f\"Created base model fileset: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model fileset already exists. Skipping creation.\")\n base_model_fs = client.files.filesets.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n print(f\"Created Model Entity: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model already exists. Updating fileset if different.\")\n base_model = client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n\nprint(f\"\\nBase model fileset: fileset://default/{base_model.name}\")\nprint(\"Base model fileset files list:\")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace=\"default\").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint(\"\\nWaiting for ModelSpec to be populated...\")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds\")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\nprint(f\"ModelSpec populated: {base_model.spec}\")", "language": "python", - "source_html": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = "meta-llama/Llama-3.2-1B-Instruct"\nMODEL_NAME = "llama-3-2-1b-base"\n\n# Ensure you have a HuggingFace token secret created\n# Create a fileset pointing to the desired HuggingFace model\ntry:\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="Llama 3.2 1B base model from HuggingFace",\n storage=HuggingfaceStorageConfigParam(\n type="huggingface",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type="model",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\n print(f"Created base model fileset: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model fileset already exists. Skipping creation.")\n base_model_fs = client.files.filesets.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n print(f"Created Model Entity: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model already exists. Updating fileset if different.")\n base_model = client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n\nprint(f"\\nBase model fileset: fileset://default/{base_model.name}")\nprint("Base model fileset files list:")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace="default").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint("\\nWaiting for ModelSpec to be populated...")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\nprint(f"ModelSpec populated: {base_model.spec}")\n" + "source_html": "import time\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = "meta-llama/Llama-3.2-1B-Instruct"\nMODEL_NAME = "llama-3-2-1b-base"\n\n# Ensure you have a Hugging Face token secret created\n# Create a fileset pointing to the desired Hugging Face model\ntry:\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="Llama 3.2 1B base model from Hugging Face",\n storage=HuggingfaceStorageConfigParam(\n type="huggingface",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type="model",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\n print(f"Created base model fileset: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model fileset already exists. Skipping creation.")\n base_model_fs = client.files.filesets.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n print(f"Created Model Entity: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model already exists. Updating fileset if different.")\n base_model = client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n\nprint(f"\\nBase model fileset: fileset://default/{base_model.name}")\nprint("Base model fileset files list:")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace="default").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint("\\nWaiting for ModelSpec to be populated...")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\nprint(f"ModelSpec populated: {base_model.spec}")\n" }, { "type": "markdown", @@ -94,8 +94,8 @@ export default { cells: [ }, { "type": "markdown", - "source": "### 6. Track Finetuning Progress\n\nA training job contains multiple steps: \n- Model and dataset downloading\n- Finetuning where LoRA adapter weights are trained\n- Creating a fileset entry for the finetuned model\n- Finetuned weights uploading\n\nThe elapsed time printed below reflects progress of the entire job. We compare the time taken by the finetuning step for both jobs in the last section of this tutorial.", - "source_html": "

6. Track Finetuning Progress

\n

A training job contains multiple steps:

\n
    \n
  • Model and dataset downloading
  • \n
  • Finetuning where LoRA adapter weights are trained
  • \n
  • Creating a fileset entry for the finetuned model
  • \n
  • Finetuned weights uploading
  • \n
\n

The elapsed time printed below reflects progress of the entire job. We compare the time taken by the finetuning step for both jobs in the last section of this tutorial.

\n" + "source": "### 6. Track Fine-Tuning Progress\n\nA training job contains multiple steps: \n- Model and dataset downloading\n- Fine-tuning where LoRA adapter weights are trained\n- Creating a fileset entry for the fine-tuned model\n- Fine-tuned weights uploading\n\nThe elapsed time printed below reflects progress of the entire job. We compare the time taken by the fine-tuning step for both jobs in the last section of this tutorial.", + "source_html": "

6. Track Fine-Tuning Progress

\n

A training job contains multiple steps:

\n
    \n
  • Model and dataset downloading
  • \n
  • Fine-tuning where LoRA adapter weights are trained
  • \n
  • Creating a fileset entry for the fine-tuned model
  • \n
  • Fine-tuned weights uploading
  • \n
\n

The elapsed time printed below reflects progress of the entire job. We compare the time taken by the fine-tuning step for both jobs in the last section of this tutorial.

\n" }, { "type": "markdown", @@ -110,14 +110,14 @@ export default { cells: [ }, { "type": "markdown", - "source": "#### Monitor the Job Until Completion\n\nThe cell below polls the job status every 10 seconds and renders a live dashboard with validation loss, GPU VRAM usage, and GPU utilization charts. The charts appear empty at first while the model and dataset download; training metrics and GPU activity populate after the finetuning step begins.\n\n> **Note:** This is additional code. You can also use the Weights & Biases or MLflow integrations.", - "source_html": "

Monitor the Job Until Completion

\n

The cell below polls the job status every 10 seconds and renders a live dashboard with validation loss, GPU VRAM usage, and GPU utilization charts. The charts appear empty at first while the model and dataset download; training metrics and GPU activity populate after the finetuning step begins.

\n
\n

Note: This is additional code. You can also use the Weights & Biases or MLflow integrations.

\n
\n" + "source": "#### Monitor the Job Until Completion\n\nThe cell below polls the job status every 10 seconds and renders a live dashboard with validation loss, GPU VRAM usage, and GPU utilization charts. The charts appear empty at first while the model and dataset download; training metrics and GPU activity populate after the fine-tuning step begins.\n\n> **Note:** This is additional code. You can also use the Weights & Biases or MLflow integrations.", + "source_html": "

Monitor the Job Until Completion

\n

The cell below polls the job status every 10 seconds and renders a live dashboard with validation loss, GPU VRAM usage, and GPU utilization charts. The charts appear empty at first while the model and dataset download; training metrics and GPU activity populate after the fine-tuning step begins.

\n
\n

Note: This is additional code. You can also use the Weights & Biases or MLflow integrations.

\n
\n" }, { "type": "code", - "source": "import time\nfrom typing import cast\nfrom IPython.display import clear_output\nfrom nemo_platform.types.shared import PlatformJobStatusResponse\n\n# Timeout set to 30 minutes to accommodate typical LoRA training duration for this dataset size.\n# Actual training time will vary based on hardware, model size, and dataset complexity.\nTIMEOUT_SECONDS = 30 * 60 # 30 minutes\nVAL_LOSS_KEY = \"val_loss\"\nTRAIN_LOSS_KEY = \"loss\"\n\n# ---------------------------------------------------------------------------\n# Job polling with live dashboard\n# ---------------------------------------------------------------------------\n\ndef wait_for_job(\n workspace: str,\n job_name: str,\n timeout: int = TIMEOUT_SECONDS,\n poll_interval: int = 10,\n val_loss_key: str = VAL_LOSS_KEY,\n train_loss_key: str = TRAIN_LOSS_KEY,\n) -> PlatformJobStatusResponse:\n \"\"\"\n Poll job status until completed, failed, cancelled, or timeout.\n Displays a live dashboard with loss curves and GPU metrics.\n\n Args:\n workspace: The workspace where the job is running.\n job_name: The name of the job to monitor.\n timeout: Maximum time to wait in seconds (default: 30 minutes).\n poll_interval: Time between status checks in seconds (default: 10).\n\n Returns:\n The final job status response.\n \"\"\"\n start_time = time.time()\n\n # Time-series accumulators required for plotting\n elapsed_mins: list[float] = []\n val_losses: list[float | None] = []\n train_losses: list[float | None] = []\n vram_history: list[list[float]] = []\n util_history: list[list[float]] = []\n\n while True:\n elapsed = time.time() - start_time\n elapsed_min = elapsed / 60\n\n # Check for timeout\n if elapsed > timeout:\n error_message = f\"Timeout reached after {elapsed_min:.1f} minutes\"\n print(f\"\\n{error_message}\")\n print(\"Job did not complete within the timeout period.\")\n raise Exception(error_message)\n\n status = client.jobs.get_status(name=job_name, workspace=workspace)\n\n # -- Extract training progress from nested steps structure --\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n val_loss: float | None = None\n train_loss: float | None = None\n current_step_name: str | None = None\n current_step_phase: str | None = None\n\n for job_step in status.steps or []:\n # Track the current active step name and phase for progress display\n if job_step.tasks:\n task = job_step.tasks[0]\n td = task.status_details or {}\n phase = cast(str, td.get(\"phase\", \"\"))\n # Update current step if it's active or pending (not completed)\n if job_step.status in (\"active\", \"pending\"):\n current_step_name = job_step.name\n current_step_phase = phase or \"started\"\n\n if job_step.name == \"training\":\n for task in job_step.tasks or []:\n td = task.status_details or {}\n step = cast(int, td[\"step\"]) if \"step\" in td else None\n max_steps = cast(int, td[\"max_steps\"]) if \"max_steps\" in td else None\n training_phase = cast(str, td[\"phase\"]) if \"phase\" in td else None\n raw_val_loss = td.get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n raw_train_loss = td.get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n break\n break\n\n if val_loss is None:\n raw_val_loss = (status.status_details or {}).get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n if train_loss is None:\n raw_train_loss = (status.status_details or {}).get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n\n # -- Collect GPU snapshot --\n vram_pcts, util_pcts = _get_gpu_snapshot()\n\n # -- Append to accumulators used for the plots --\n elapsed_mins.append(elapsed_min)\n val_losses.append(val_loss)\n train_losses.append(train_loss)\n vram_history.append(vram_pcts)\n util_history.append(util_pcts)\n\n # -- Build status strings --\n status_str = f\"Status: {status.status}\"\n if step is not None and max_steps is not None:\n pct = step / max_steps * 100\n step_str = f\"Step {step}/{max_steps} ({pct:.0f}%)\"\n if training_phase:\n step_str += f\" - {training_phase}\"\n else:\n if current_step_name and current_step_phase:\n step_str = f\"{current_step_name} - {current_step_phase}\"\n elif current_step_name:\n step_str = f\"{current_step_name}\"\n else:\n step_str = \"Waiting for training to start...\"\n elapsed_str = f\"Elapsed: {elapsed_min:.1f} min\"\n\n # -- Redraw dashboard --\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n\n # -- Check terminal conditions --\n if status.status.lower() == \"completed\":\n # Redraw dashboard one final time with \"completed\" status\n status_str = f\"Status: {status.status}\"\n if step is not None and max_steps is not None:\n step_str = f\"Step {max_steps}/{max_steps} (100%)\"\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n print(f\"\\nJob completed in {elapsed_min:.1f} minutes ({elapsed:.0f}s)\")\n return status\n elif status.status.lower() in (\"failed\", \"cancelled\", \"error\"):\n print(f\"\\nJob finished with status: {status.status}\")\n print(f\"Total time elapsed: {elapsed_min:.1f} minutes ({elapsed:.0f}s)\")\n\n # Print error details from the job level\n if status.error_details:\n error_msg = status.error_details.get(\"message\", \"\")\n if error_msg:\n print(f\"\\nError: {error_msg}\")\n\n # Find and print error details from the failed step/task\n for job_step in status.steps or []:\n if job_step.status == \"error\":\n print(f\"\\nFailed step: {job_step.name}\")\n if job_step.error_details:\n step_error = job_step.error_details.get(\"message\", \"\")\n if step_error:\n print(f\"Step error: {step_error}\")\n # Get error_stack from the failed task\n for task in job_step.tasks or []:\n if task.status == \"error\" and hasattr(task, \"error_stack\") and task.error_stack:\n print(f\"\\nError stack trace:\\n{task.error_stack}\")\n elif task.status == \"error\" and task.error_details:\n task_error = task.error_details.get(\"message\", \"\")\n if task_error:\n print(f\"Task error: {task_error}\")\n break\n\n raise Exception(f\"Job finished with status: {status.status}\")\n\n time.sleep(poll_interval)\n\n\n# Wait for the job to complete\njob_with_sequence_packing_status = wait_for_job(\n workspace=\"default\",\n job_name=job_with_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS,\n)\n\npacked_val_loss = (job_with_sequence_packing_status.status_details or {}).get(\"val_loss\")\nif packed_val_loss is not None:\n print(f\"Validation loss: {float(packed_val_loss):.2f}\")\nelse:\n print(\"Validation loss: not reported in job status\")", + "source": "import time\nfrom typing import cast\nfrom IPython.display import clear_output\nfrom nemo_platform.types.shared import PlatformJobStatusResponse\n\n# Timeout set to 30 minutes to accommodate typical LoRA training duration for this dataset size.\n# Actual training time will vary based on hardware, model size, and dataset complexity.\nTIMEOUT_SECONDS = 30 * 60 # 30 minutes\nVAL_LOSS_KEY = \"val_loss\"\nTRAIN_LOSS_KEY = \"train_loss\"\n\n\ndef get_training_metric(\n status: PlatformJobStatusResponse,\n metric_key: str,\n) -> float | None:\n \"\"\"Return a metric reported by a task in the training step.\"\"\"\n for job_step in status.steps or []:\n if job_step.name == \"training\":\n for task in job_step.tasks or []:\n value = (task.status_details or {}).get(metric_key)\n if value is not None:\n return float(value)\n return None\n\n\n# ---------------------------------------------------------------------------\n# Job polling with live dashboard\n# ---------------------------------------------------------------------------\n\ndef wait_for_job(\n workspace: str,\n job_name: str,\n timeout: int = TIMEOUT_SECONDS,\n poll_interval: int = 10,\n val_loss_key: str = VAL_LOSS_KEY,\n train_loss_key: str = TRAIN_LOSS_KEY,\n) -> PlatformJobStatusResponse:\n \"\"\"\n Poll job status until completed, failed, cancelled, or timeout.\n Displays a live dashboard with loss curves and GPU metrics.\n\n Args:\n workspace: The workspace where the job is running.\n job_name: The name of the job to monitor.\n timeout: Maximum time to wait in seconds (default: 30 minutes).\n poll_interval: Time between status checks in seconds (default: 10).\n\n Returns:\n The final job status response.\n \"\"\"\n start_time = time.time()\n\n # Time-series accumulators required for plotting\n elapsed_mins: list[float] = []\n val_losses: list[float | None] = []\n train_losses: list[float | None] = []\n vram_history: list[list[float]] = []\n util_history: list[list[float]] = []\n\n while True:\n elapsed = time.time() - start_time\n elapsed_min = elapsed / 60\n\n # Check for timeout\n if elapsed > timeout:\n error_message = f\"Timeout reached after {elapsed_min:.1f} minutes\"\n print(f\"\\n{error_message}\")\n print(\"Job did not complete within the timeout period.\")\n raise Exception(error_message)\n\n status = client.jobs.get_status(name=job_name, workspace=workspace)\n\n # -- Extract training progress from nested steps structure --\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n val_loss: float | None = None\n train_loss: float | None = None\n current_step_name: str | None = None\n current_step_phase: str | None = None\n\n for job_step in status.steps or []:\n # Track the current active step name and phase for progress display\n if job_step.tasks:\n task = job_step.tasks[0]\n td = task.status_details or {}\n phase = cast(str, td.get(\"phase\", \"\"))\n # Update current step if it's active or pending (not completed)\n if job_step.status in (\"active\", \"pending\"):\n current_step_name = job_step.name\n current_step_phase = phase or \"started\"\n\n if job_step.name == \"training\":\n for task in job_step.tasks or []:\n td = task.status_details or {}\n step = cast(int, td[\"step\"]) if \"step\" in td else None\n max_steps = cast(int, td[\"max_steps\"]) if \"max_steps\" in td else None\n training_phase = cast(str, td[\"phase\"]) if \"phase\" in td else None\n raw_val_loss = td.get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n raw_train_loss = td.get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n break\n break\n\n if val_loss is None:\n raw_val_loss = (status.status_details or {}).get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n if train_loss is None:\n raw_train_loss = (status.status_details or {}).get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n\n # -- Collect GPU snapshot --\n vram_pcts, util_pcts = _get_gpu_snapshot()\n\n # -- Append to accumulators used for the plots --\n elapsed_mins.append(elapsed_min)\n val_losses.append(val_loss)\n train_losses.append(train_loss)\n vram_history.append(vram_pcts)\n util_history.append(util_pcts)\n\n # -- Build status strings --\n status_str = f\"Status: {status.status}\"\n if step is not None and max_steps is not None:\n pct = step / max_steps * 100\n step_str = f\"Step {step}/{max_steps} ({pct:.0f}%)\"\n if training_phase:\n step_str += f\" - {training_phase}\"\n else:\n if current_step_name and current_step_phase:\n step_str = f\"{current_step_name} - {current_step_phase}\"\n elif current_step_name:\n step_str = f\"{current_step_name}\"\n else:\n step_str = \"Waiting for training to start...\"\n elapsed_str = f\"Elapsed: {elapsed_min:.1f} min\"\n\n # -- Redraw dashboard --\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n\n # -- Check terminal conditions --\n if status.status.lower() == \"completed\":\n # Redraw dashboard one final time with \"completed\" status\n status_str = f\"Status: {status.status}\"\n if step is not None and max_steps is not None:\n step_str = f\"Step {max_steps}/{max_steps} (100%)\"\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n print(f\"\\nJob completed in {elapsed_min:.1f} minutes ({elapsed:.0f}s)\")\n return status\n elif status.status.lower() in (\"failed\", \"cancelled\", \"error\"):\n print(f\"\\nJob finished with status: {status.status}\")\n print(f\"Total time elapsed: {elapsed_min:.1f} minutes ({elapsed:.0f}s)\")\n\n # Print error details from the job level\n if status.error_details:\n error_msg = status.error_details.get(\"message\", \"\")\n if error_msg:\n print(f\"\\nError: {error_msg}\")\n\n # Find and print error details from the failed step/task\n for job_step in status.steps or []:\n if job_step.status == \"error\":\n print(f\"\\nFailed step: {job_step.name}\")\n if job_step.error_details:\n step_error = job_step.error_details.get(\"message\", \"\")\n if step_error:\n print(f\"Step error: {step_error}\")\n # Get error_stack from the failed task\n for task in job_step.tasks or []:\n if task.status == \"error\" and hasattr(task, \"error_stack\") and task.error_stack:\n print(f\"\\nError stack trace:\\n{task.error_stack}\")\n elif task.status == \"error\" and task.error_details:\n task_error = task.error_details.get(\"message\", \"\")\n if task_error:\n print(f\"Task error: {task_error}\")\n break\n\n raise Exception(f\"Job finished with status: {status.status}\")\n\n time.sleep(poll_interval)\n\n\n# Wait for the job to complete\njob_with_sequence_packing_status = wait_for_job(\n workspace=\"default\",\n job_name=job_with_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS,\n)\n\npacked_val_loss = get_training_metric(job_with_sequence_packing_status, VAL_LOSS_KEY)\nif packed_val_loss is not None:\n print(f\"Validation loss: {packed_val_loss:.2f}\")\nelse:\n print(\"Validation loss: not reported in job status\")", "language": "python", - "source_html": "import time\nfrom typing import cast\nfrom IPython.display import clear_output\nfrom nemo_platform.types.shared import PlatformJobStatusResponse\n\n# Timeout set to 30 minutes to accommodate typical LoRA training duration for this dataset size.\n# Actual training time will vary based on hardware, model size, and dataset complexity.\nTIMEOUT_SECONDS = 30 * 60 # 30 minutes\nVAL_LOSS_KEY = "val_loss"\nTRAIN_LOSS_KEY = "loss"\n\n# ---------------------------------------------------------------------------\n# Job polling with live dashboard\n# ---------------------------------------------------------------------------\n\ndef wait_for_job(\n workspace: str,\n job_name: str,\n timeout: int = TIMEOUT_SECONDS,\n poll_interval: int = 10,\n val_loss_key: str = VAL_LOSS_KEY,\n train_loss_key: str = TRAIN_LOSS_KEY,\n) -> PlatformJobStatusResponse:\n """\n Poll job status until completed, failed, cancelled, or timeout.\n Displays a live dashboard with loss curves and GPU metrics.\n\n Args:\n workspace: The workspace where the job is running.\n job_name: The name of the job to monitor.\n timeout: Maximum time to wait in seconds (default: 30 minutes).\n poll_interval: Time between status checks in seconds (default: 10).\n\n Returns:\n The final job status response.\n """\n start_time = time.time()\n\n # Time-series accumulators required for plotting\n elapsed_mins: list[float] = []\n val_losses: list[float | None] = []\n train_losses: list[float | None] = []\n vram_history: list[list[float]] = []\n util_history: list[list[float]] = []\n\n while True:\n elapsed = time.time() - start_time\n elapsed_min = elapsed / 60\n\n # Check for timeout\n if elapsed > timeout:\n error_message = f"Timeout reached after {elapsed_min:.1f} minutes"\n print(f"\\n{error_message}")\n print("Job did not complete within the timeout period.")\n raise Exception(error_message)\n\n status = client.jobs.get_status(name=job_name, workspace=workspace)\n\n # -- Extract training progress from nested steps structure --\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n val_loss: float | None = None\n train_loss: float | None = None\n current_step_name: str | None = None\n current_step_phase: str | None = None\n\n for job_step in status.steps or []:\n # Track the current active step name and phase for progress display\n if job_step.tasks:\n task = job_step.tasks[0]\n td = task.status_details or {}\n phase = cast(str, td.get("phase", ""))\n # Update current step if it's active or pending (not completed)\n if job_step.status in ("active", "pending"):\n current_step_name = job_step.name\n current_step_phase = phase or "started"\n\n if job_step.name == "training":\n for task in job_step.tasks or []:\n td = task.status_details or {}\n step = cast(int, td["step"]) if "step" in td else None\n max_steps = cast(int, td["max_steps"]) if "max_steps" in td else None\n training_phase = cast(str, td["phase"]) if "phase" in td else None\n raw_val_loss = td.get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n raw_train_loss = td.get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n break\n break\n\n if val_loss is None:\n raw_val_loss = (status.status_details or {}).get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n if train_loss is None:\n raw_train_loss = (status.status_details or {}).get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n\n # -- Collect GPU snapshot --\n vram_pcts, util_pcts = _get_gpu_snapshot()\n\n # -- Append to accumulators used for the plots --\n elapsed_mins.append(elapsed_min)\n val_losses.append(val_loss)\n train_losses.append(train_loss)\n vram_history.append(vram_pcts)\n util_history.append(util_pcts)\n\n # -- Build status strings --\n status_str = f"Status: {status.status}"\n if step is not None and max_steps is not None:\n pct = step / max_steps * 100\n step_str = f"Step {step}/{max_steps} ({pct:.0f}%)"\n if training_phase:\n step_str += f" - {training_phase}"\n else:\n if current_step_name and current_step_phase:\n step_str = f"{current_step_name} - {current_step_phase}"\n elif current_step_name:\n step_str = f"{current_step_name}"\n else:\n step_str = "Waiting for training to start..."\n elapsed_str = f"Elapsed: {elapsed_min:.1f} min"\n\n # -- Redraw dashboard --\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n\n # -- Check terminal conditions --\n if status.status.lower() == "completed":\n # Redraw dashboard one final time with "completed" status\n status_str = f"Status: {status.status}"\n if step is not None and max_steps is not None:\n step_str = f"Step {max_steps}/{max_steps} (100%)"\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n print(f"\\nJob completed in {elapsed_min:.1f} minutes ({elapsed:.0f}s)")\n return status\n elif status.status.lower() in ("failed", "cancelled", "error"):\n print(f"\\nJob finished with status: {status.status}")\n print(f"Total time elapsed: {elapsed_min:.1f} minutes ({elapsed:.0f}s)")\n\n # Print error details from the job level\n if status.error_details:\n error_msg = status.error_details.get("message", "")\n if error_msg:\n print(f"\\nError: {error_msg}")\n\n # Find and print error details from the failed step/task\n for job_step in status.steps or []:\n if job_step.status == "error":\n print(f"\\nFailed step: {job_step.name}")\n if job_step.error_details:\n step_error = job_step.error_details.get("message", "")\n if step_error:\n print(f"Step error: {step_error}")\n # Get error_stack from the failed task\n for task in job_step.tasks or []:\n if task.status == "error" and hasattr(task, "error_stack") and task.error_stack:\n print(f"\\nError stack trace:\\n{task.error_stack}")\n elif task.status == "error" and task.error_details:\n task_error = task.error_details.get("message", "")\n if task_error:\n print(f"Task error: {task_error}")\n break\n\n raise Exception(f"Job finished with status: {status.status}")\n\n time.sleep(poll_interval)\n\n\n# Wait for the job to complete\njob_with_sequence_packing_status = wait_for_job(\n workspace="default",\n job_name=job_with_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS,\n)\n\npacked_val_loss = (job_with_sequence_packing_status.status_details or {}).get("val_loss")\nif packed_val_loss is not None:\n print(f"Validation loss: {float(packed_val_loss):.2f}")\nelse:\n print("Validation loss: not reported in job status")\n" + "source_html": "import time\nfrom typing import cast\nfrom IPython.display import clear_output\nfrom nemo_platform.types.shared import PlatformJobStatusResponse\n\n# Timeout set to 30 minutes to accommodate typical LoRA training duration for this dataset size.\n# Actual training time will vary based on hardware, model size, and dataset complexity.\nTIMEOUT_SECONDS = 30 * 60 # 30 minutes\nVAL_LOSS_KEY = "val_loss"\nTRAIN_LOSS_KEY = "train_loss"\n\n\ndef get_training_metric(\n status: PlatformJobStatusResponse,\n metric_key: str,\n) -> float | None:\n """Return a metric reported by a task in the training step."""\n for job_step in status.steps or []:\n if job_step.name == "training":\n for task in job_step.tasks or []:\n value = (task.status_details or {}).get(metric_key)\n if value is not None:\n return float(value)\n return None\n\n\n# ---------------------------------------------------------------------------\n# Job polling with live dashboard\n# ---------------------------------------------------------------------------\n\ndef wait_for_job(\n workspace: str,\n job_name: str,\n timeout: int = TIMEOUT_SECONDS,\n poll_interval: int = 10,\n val_loss_key: str = VAL_LOSS_KEY,\n train_loss_key: str = TRAIN_LOSS_KEY,\n) -> PlatformJobStatusResponse:\n """\n Poll job status until completed, failed, cancelled, or timeout.\n Displays a live dashboard with loss curves and GPU metrics.\n\n Args:\n workspace: The workspace where the job is running.\n job_name: The name of the job to monitor.\n timeout: Maximum time to wait in seconds (default: 30 minutes).\n poll_interval: Time between status checks in seconds (default: 10).\n\n Returns:\n The final job status response.\n """\n start_time = time.time()\n\n # Time-series accumulators required for plotting\n elapsed_mins: list[float] = []\n val_losses: list[float | None] = []\n train_losses: list[float | None] = []\n vram_history: list[list[float]] = []\n util_history: list[list[float]] = []\n\n while True:\n elapsed = time.time() - start_time\n elapsed_min = elapsed / 60\n\n # Check for timeout\n if elapsed > timeout:\n error_message = f"Timeout reached after {elapsed_min:.1f} minutes"\n print(f"\\n{error_message}")\n print("Job did not complete within the timeout period.")\n raise Exception(error_message)\n\n status = client.jobs.get_status(name=job_name, workspace=workspace)\n\n # -- Extract training progress from nested steps structure --\n step: int | None = None\n max_steps: int | None = None\n training_phase: str | None = None\n val_loss: float | None = None\n train_loss: float | None = None\n current_step_name: str | None = None\n current_step_phase: str | None = None\n\n for job_step in status.steps or []:\n # Track the current active step name and phase for progress display\n if job_step.tasks:\n task = job_step.tasks[0]\n td = task.status_details or {}\n phase = cast(str, td.get("phase", ""))\n # Update current step if it's active or pending (not completed)\n if job_step.status in ("active", "pending"):\n current_step_name = job_step.name\n current_step_phase = phase or "started"\n\n if job_step.name == "training":\n for task in job_step.tasks or []:\n td = task.status_details or {}\n step = cast(int, td["step"]) if "step" in td else None\n max_steps = cast(int, td["max_steps"]) if "max_steps" in td else None\n training_phase = cast(str, td["phase"]) if "phase" in td else None\n raw_val_loss = td.get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n raw_train_loss = td.get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n break\n break\n\n if val_loss is None:\n raw_val_loss = (status.status_details or {}).get(val_loss_key)\n val_loss = float(raw_val_loss) if raw_val_loss is not None else None\n if train_loss is None:\n raw_train_loss = (status.status_details or {}).get(train_loss_key)\n train_loss = float(raw_train_loss) if raw_train_loss is not None else None\n\n # -- Collect GPU snapshot --\n vram_pcts, util_pcts = _get_gpu_snapshot()\n\n # -- Append to accumulators used for the plots --\n elapsed_mins.append(elapsed_min)\n val_losses.append(val_loss)\n train_losses.append(train_loss)\n vram_history.append(vram_pcts)\n util_history.append(util_pcts)\n\n # -- Build status strings --\n status_str = f"Status: {status.status}"\n if step is not None and max_steps is not None:\n pct = step / max_steps * 100\n step_str = f"Step {step}/{max_steps} ({pct:.0f}%)"\n if training_phase:\n step_str += f" - {training_phase}"\n else:\n if current_step_name and current_step_phase:\n step_str = f"{current_step_name} - {current_step_phase}"\n elif current_step_name:\n step_str = f"{current_step_name}"\n else:\n step_str = "Waiting for training to start..."\n elapsed_str = f"Elapsed: {elapsed_min:.1f} min"\n\n # -- Redraw dashboard --\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n\n # -- Check terminal conditions --\n if status.status.lower() == "completed":\n # Redraw dashboard one final time with "completed" status\n status_str = f"Status: {status.status}"\n if step is not None and max_steps is not None:\n step_str = f"Step {max_steps}/{max_steps} (100%)"\n clear_output(wait=True)\n _draw_dashboard(\n elapsed_mins, val_losses, train_losses,\n vram_history, util_history,\n job_name, status_str, step_str, elapsed_str,\n )\n print(f"\\nJob completed in {elapsed_min:.1f} minutes ({elapsed:.0f}s)")\n return status\n elif status.status.lower() in ("failed", "cancelled", "error"):\n print(f"\\nJob finished with status: {status.status}")\n print(f"Total time elapsed: {elapsed_min:.1f} minutes ({elapsed:.0f}s)")\n\n # Print error details from the job level\n if status.error_details:\n error_msg = status.error_details.get("message", "")\n if error_msg:\n print(f"\\nError: {error_msg}")\n\n # Find and print error details from the failed step/task\n for job_step in status.steps or []:\n if job_step.status == "error":\n print(f"\\nFailed step: {job_step.name}")\n if job_step.error_details:\n step_error = job_step.error_details.get("message", "")\n if step_error:\n print(f"Step error: {step_error}")\n # Get error_stack from the failed task\n for task in job_step.tasks or []:\n if task.status == "error" and hasattr(task, "error_stack") and task.error_stack:\n print(f"\\nError stack trace:\\n{task.error_stack}")\n elif task.status == "error" and task.error_details:\n task_error = task.error_details.get("message", "")\n if task_error:\n print(f"Task error: {task_error}")\n break\n\n raise Exception(f"Job finished with status: {status.status}")\n\n time.sleep(poll_interval)\n\n\n# Wait for the job to complete\njob_with_sequence_packing_status = wait_for_job(\n workspace="default",\n job_name=job_with_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS,\n)\n\npacked_val_loss = get_training_metric(job_with_sequence_packing_status, VAL_LOSS_KEY)\nif packed_val_loss is not None:\n print(f"Validation loss: {packed_val_loss:.2f}")\nelse:\n print("Validation loss: not reported in job status")\n" }, { "type": "markdown", @@ -132,14 +132,14 @@ export default { cells: [ }, { "type": "markdown", - "source": "### 8. Track Finetuning Progress for Job without Sequence Packing", - "source_html": "

8. Track Finetuning Progress for Job without Sequence Packing

\n" + "source": "### 8. Track Fine-Tuning Progress for Job without Sequence Packing", + "source_html": "

8. Track Fine-Tuning Progress for Job without Sequence Packing

\n" }, { "type": "code", - "source": "# Wait for the training step to complete\njob_without_sequence_packing_status = wait_for_job(\n workspace=\"default\",\n job_name=job_without_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS\n)\n\nno_pack_val_loss = (job_without_sequence_packing_status.status_details or {}).get(\"val_loss\")\nif no_pack_val_loss is not None:\n print(f\"Validation loss: {float(no_pack_val_loss):.2f}\")\nelse:\n print(\"Validation loss: not reported in job status\")", + "source": "# Wait for the training step to complete\njob_without_sequence_packing_status = wait_for_job(\n workspace=\"default\",\n job_name=job_without_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS\n)\n\nno_pack_val_loss = get_training_metric(job_without_sequence_packing_status, VAL_LOSS_KEY)\nif no_pack_val_loss is not None:\n print(f\"Validation loss: {no_pack_val_loss:.2f}\")\nelse:\n print(\"Validation loss: not reported in job status\")", "language": "python", - "source_html": "# Wait for the training step to complete\njob_without_sequence_packing_status = wait_for_job(\n workspace="default",\n job_name=job_without_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS\n)\n\nno_pack_val_loss = (job_without_sequence_packing_status.status_details or {}).get("val_loss")\nif no_pack_val_loss is not None:\n print(f"Validation loss: {float(no_pack_val_loss):.2f}")\nelse:\n print("Validation loss: not reported in job status")\n" + "source_html": "# Wait for the training step to complete\njob_without_sequence_packing_status = wait_for_job(\n workspace="default",\n job_name=job_without_sequence_packing.job.name,\n timeout=TIMEOUT_SECONDS\n)\n\nno_pack_val_loss = get_training_metric(job_without_sequence_packing_status, VAL_LOSS_KEY)\nif no_pack_val_loss is not None:\n print(f"Validation loss: {no_pack_val_loss:.2f}")\nelse:\n print("Validation loss: not reported in job status")\n" }, { "type": "markdown", @@ -148,13 +148,13 @@ export default { cells: [ }, { "type": "code", - "source": "from nemo_platform.types.jobs import PlatformJobStep\nfrom datetime import datetime\nimport pandas as pd\n\nSTEP_NAME = \"training\"\n\ndef get_elapsed_time(step: PlatformJobStep) -> float:\n \"\"\"Calculate elapsed time in seconds from step's created_at to updated_at.\"\"\"\n created_at = datetime.fromisoformat(step.created_at.replace(\"Z\", \"+00:00\"))\n updated_at = datetime.fromisoformat(step.updated_at.replace(\"Z\", \"+00:00\"))\n return (updated_at - created_at).total_seconds()\n\nstep_with_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace=\"default\",\n job=job_with_sequence_packing.job.name,\n)\n\nstep_without_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace=\"default\",\n job=job_without_sequence_packing.job.name,\n)\n\ntime_to_complete_with_sequence_packing = get_elapsed_time(step_with_sequence_packing)\ntime_to_complete_without_sequence_packing = get_elapsed_time(step_without_sequence_packing)\n\n# Display results as a table\nresults_df = pd.DataFrame({\n \"Seq Packing Enabled\": [True, False],\n \"Val Loss\": [\n (job_with_sequence_packing_status.status_details or {}).get(\"val_loss\"),\n (job_without_sequence_packing_status.status_details or {}).get(\"val_loss\"),\n ],\n \"Training Step Time, sec\": [\n time_to_complete_with_sequence_packing,\n time_to_complete_without_sequence_packing\n ]\n})\n\nresults_df.style.format({\"Val Loss\": \"{:.2f}\", \"Training Step Time, sec\": \"{:.0f}\"}).hide(axis='index')", + "source": "from nemo_platform.types.jobs import PlatformJobStep\nimport pandas as pd\n\nSTEP_NAME = \"training\"\n\ndef get_elapsed_time(step: PlatformJobStep) -> float:\n \"\"\"Calculate elapsed time in seconds from step's created_at to updated_at.\"\"\"\n if step.created_at is None or step.updated_at is None:\n raise ValueError(\"Training step timestamps are unavailable\")\n return (step.updated_at - step.created_at).total_seconds()\n\nstep_with_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace=\"default\",\n job=job_with_sequence_packing.job.name,\n)\n\nstep_without_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace=\"default\",\n job=job_without_sequence_packing.job.name,\n)\n\ntime_to_complete_with_sequence_packing = get_elapsed_time(step_with_sequence_packing)\ntime_to_complete_without_sequence_packing = get_elapsed_time(step_without_sequence_packing)\n\n# Display results as a table\nresults_df = pd.DataFrame({\n \"Seq Packing Enabled\": [True, False],\n \"Val Loss\": [packed_val_loss, no_pack_val_loss],\n \"Training Step Time, sec\": [\n time_to_complete_with_sequence_packing,\n time_to_complete_without_sequence_packing\n ]\n})\n\nresults_df.style.format({\"Val Loss\": \"{:.2f}\", \"Training Step Time, sec\": \"{:.0f}\"}).hide(axis='index')", "language": "python", - "source_html": "from nemo_platform.types.jobs import PlatformJobStep\nfrom datetime import datetime\nimport pandas as pd\n\nSTEP_NAME = "training"\n\ndef get_elapsed_time(step: PlatformJobStep) -> float:\n """Calculate elapsed time in seconds from step's created_at to updated_at."""\n created_at = datetime.fromisoformat(step.created_at.replace("Z", "+00:00"))\n updated_at = datetime.fromisoformat(step.updated_at.replace("Z", "+00:00"))\n return (updated_at - created_at).total_seconds()\n\nstep_with_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace="default",\n job=job_with_sequence_packing.job.name,\n)\n\nstep_without_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace="default",\n job=job_without_sequence_packing.job.name,\n)\n\ntime_to_complete_with_sequence_packing = get_elapsed_time(step_with_sequence_packing)\ntime_to_complete_without_sequence_packing = get_elapsed_time(step_without_sequence_packing)\n\n# Display results as a table\nresults_df = pd.DataFrame({\n "Seq Packing Enabled": [True, False],\n "Val Loss": [\n (job_with_sequence_packing_status.status_details or {}).get("val_loss"),\n (job_without_sequence_packing_status.status_details or {}).get("val_loss"),\n ],\n "Training Step Time, sec": [\n time_to_complete_with_sequence_packing,\n time_to_complete_without_sequence_packing\n ]\n})\n\nresults_df.style.format({"Val Loss": "{:.2f}", "Training Step Time, sec": "{:.0f}"}).hide(axis='index')\n" + "source_html": "from nemo_platform.types.jobs import PlatformJobStep\nimport pandas as pd\n\nSTEP_NAME = "training"\n\ndef get_elapsed_time(step: PlatformJobStep) -> float:\n """Calculate elapsed time in seconds from step's created_at to updated_at."""\n if step.created_at is None or step.updated_at is None:\n raise ValueError("Training step timestamps are unavailable")\n return (step.updated_at - step.created_at).total_seconds()\n\nstep_with_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace="default",\n job=job_with_sequence_packing.job.name,\n)\n\nstep_without_sequence_packing = client.jobs.steps.retrieve(\n name=STEP_NAME,\n workspace="default",\n job=job_without_sequence_packing.job.name,\n)\n\ntime_to_complete_with_sequence_packing = get_elapsed_time(step_with_sequence_packing)\ntime_to_complete_without_sequence_packing = get_elapsed_time(step_without_sequence_packing)\n\n# Display results as a table\nresults_df = pd.DataFrame({\n "Seq Packing Enabled": [True, False],\n "Val Loss": [packed_val_loss, no_pack_val_loss],\n "Training Step Time, sec": [\n time_to_complete_with_sequence_packing,\n time_to_complete_without_sequence_packing\n ]\n})\n\nresults_df.style.format({"Val Loss": "{:.2f}", "Training Step Time, sec": "{:.0f}"}).hide(axis='index')\n" }, { "type": "markdown", - "source": "#### Examples of Validation Loss\n\nThe expected validation loss curves should match closely for both jobs.\n![Validation loss comparison chart showing similar convergence patterns between sequence-packed and non-packed training runs over training steps](../_images/packed_vs_not_packed_val_loss.png)\n\nSequence packed version should complete significantly faster.\n![Runtime comparison chart demonstrating significantly reduced training time for sequence-packed job compared to non-packed baseline](../_images/runtime.png)\n\n#### GPU Utilization\nSequence packed version should have a higher GPU utilization.\n![GPU utilization chart showing higher and more consistent GPU usage with sequence packing enabled throughout the training process](../_images/gpu_utilization.png)\n\n#### GPU Memory Allocation\nSequence packed version should have a higher GPU Memory Allocation.\n![GPU memory allocation chart illustrating increased memory utilization efficiency with sequence packing enabled](../_images/gpu_memory.png)", - "source_html": "

Examples of Validation Loss

\n

The expected validation loss curves should match closely for both jobs.\n\"Validation

\n

Sequence packed version should complete significantly faster.\n\"Runtime

\n

GPU Utilization

\n

Sequence packed version should have a higher GPU utilization.\n\"GPU

\n

GPU Memory Allocation

\n

Sequence packed version should have a higher GPU Memory Allocation.\n\"GPU

\n" + "source": "#### Examples of Validation Loss\n\nThe expected validation loss curves should match closely for both jobs.\n![Validation loss comparison chart showing similar convergence patterns between sequence-packed and non-packed training runs over training steps](../_images/packed_vs_not_packed_val_loss.png)\n\nSequence packed version should complete significantly faster.\n![Runtime comparison chart demonstrating significantly reduced training time for sequence-packed job compared to non-packed baseline](../_images/runtime.png)\n\n#### GPU Utilization\nSequence packed version should have a higher GPU utilization.\n![GPU utilization chart showing higher and more consistent GPU usage with sequence packing enabled throughout the training process](../_images/gpu_utilization.png)\n\n#### GPU Memory Allocation\nSequence packed version should have a higher GPU Memory Allocation.\n![GPU memory allocation chart illustrating increased memory utilization efficiency with sequence packing enabled](../_images/gpu_memory.png)\n\n## Next Steps\n\n- [Monitor customization metrics](fine-tune-metrics) for training and validation loss.\n- [Create a LoRA customization job](./lora-customization-job).\n- [Create a Full SFT customization job](./sft-customization-job).", + "source_html": "

Examples of Validation Loss

\n

The expected validation loss curves should match closely for both jobs.\n\"Validation

\n

Sequence packed version should complete significantly faster.\n\"Runtime

\n

GPU Utilization

\n

Sequence packed version should have a higher GPU utilization.\n\"GPU

\n

GPU Memory Allocation

\n

Sequence packed version should have a higher GPU Memory Allocation.\n\"GPU

\n

Next Steps

\n\n" } ] }; diff --git a/docs/fern/components/notebooks/sft-customization-job.json b/docs/fern/components/notebooks/sft-customization-job.json index fc41a5d23c..33c6a61148 100644 --- a/docs/fern/components/notebooks/sft-customization-job.json +++ b/docs/fern/components/notebooks/sft-customization-job.json @@ -2,13 +2,13 @@ "cells": [ { "type": "markdown", - "source": "\n\n\n# Full SFT Customization\n\nLearn how to fine-tune all model weights using supervised fine-tuning (SFT) to customize LLM behavior for your specific tasks.\n\n## About\n\nSupervised Fine-Tuning (SFT) customizes model behavior, injects new knowledge, and optimizes performance for specific domains and tasks. Full SFT modifies **all model weights** during training, providing maximum customization flexibility.\n\n**What you can achieve with SFT:**\n\n- 🎯 **Specialize for domains:** Fine-tune models on legal texts, medical records, or financial data\n- 💡 **Inject knowledge:** Add new information not present in the base model\n- 📈 **Improve accuracy:** Optimize for specific tasks like sentiment analysis, summarization, or code generation\n\n### SFT vs LoRA: Understanding the Trade-offs\n\n**Full SFT** trains all model parameters (for example, all 70 billion weights in Llama 70B):\n\n- ✅ Maximum model adaptation and knowledge injection\n- ✅ Can fundamentally change model behavior\n- ✅ Best for significant domain shifts or specialized tasks\n- ❌ Requires substantial GPU resources (4-8x more than LoRA)\n- ❌ Produces full model weights (~140GB for Llama 70B)\n- ❌ Longer training time\n\n**LoRA** trains only ~1% of weights by adding thin matrices to existing weights:\n\n- ✅ 75-95% less memory required\n- ✅ Faster training (2-4x speedup)\n- ✅ Produces small adapter files (~100-500MB)\n- ✅ Multiple adapters can share one base model\n- ❌ Limited adaptation capability compared to full fine-tuning\n\n**When to choose Full SFT:**\n\n- Training small models (1B-8B) where resource cost is manageable\n- Need fundamental behavior changes (for example, medical diagnosis, legal reasoning)\n- Injecting substantial new knowledge not in the base model\n\n**When to choose LoRA:** Refer to the [LoRA tutorial](./lora-customization-job) for most use cases, especially with large models (70B+) or limited GPU resources.", - "source_html": "\n\n

Full SFT Customization

\n

Learn how to fine-tune all model weights using supervised fine-tuning (SFT) to customize LLM behavior for your specific tasks.

\n

About

\n

Supervised Fine-Tuning (SFT) customizes model behavior, injects new knowledge, and optimizes performance for specific domains and tasks. Full SFT modifies all model weights during training, providing maximum customization flexibility.

\n

What you can achieve with SFT:

\n
    \n
  • 🎯 Specialize for domains: Fine-tune models on legal texts, medical records, or financial data
  • \n
  • 💡 Inject knowledge: Add new information not present in the base model
  • \n
  • 📈 Improve accuracy: Optimize for specific tasks like sentiment analysis, summarization, or code generation
  • \n
\n

SFT vs LoRA: Understanding the Trade-offs

\n

Full SFT trains all model parameters (for example, all 70 billion weights in Llama 70B):

\n
    \n
  • ✅ Maximum model adaptation and knowledge injection
  • \n
  • ✅ Can fundamentally change model behavior
  • \n
  • ✅ Best for significant domain shifts or specialized tasks
  • \n
  • ❌ Requires substantial GPU resources (4-8x more than LoRA)
  • \n
  • ❌ Produces full model weights (~140GB for Llama 70B)
  • \n
  • ❌ Longer training time
  • \n
\n

LoRA trains only ~1% of weights by adding thin matrices to existing weights:

\n
    \n
  • ✅ 75-95% less memory required
  • \n
  • ✅ Faster training (2-4x speedup)
  • \n
  • ✅ Produces small adapter files (~100-500MB)
  • \n
  • ✅ Multiple adapters can share one base model
  • \n
  • ❌ Limited adaptation capability compared to full fine-tuning
  • \n
\n

When to choose Full SFT:

\n
    \n
  • Training small models (1B-8B) where resource cost is manageable
  • \n
  • Need fundamental behavior changes (for example, medical diagnosis, legal reasoning)
  • \n
  • Injecting substantial new knowledge not in the base model
  • \n
\n

When to choose LoRA: Refer to the LoRA tutorial for most use cases, especially with large models (70B+) or limited GPU resources.

\n" + "source": "\n\n\n# Full SFT Customization\n\nLearn how to fine-tune all model weights using supervised fine-tuning (SFT) to customize LLM behavior for your specific tasks.\n\n## About\n\nSupervised Fine-Tuning (SFT) customizes model behavior, injects new knowledge, and optimizes performance for specific domains and tasks. Full SFT modifies **all model weights** during training, providing maximum customization flexibility.\n\n**What you can achieve with SFT:**\n\n- 🎯 **Specialize for domains:** Fine-tune models on legal texts, medical records, or financial data\n- 💡 **Inject knowledge:** Add new information not present in the base model\n- 📈 **Improve accuracy:** Optimize for specific tasks like sentiment analysis, summarization, or code generation\n\n### SFT vs LoRA: Understanding the Trade-offs\n\n**Full SFT** trains all model parameters (for example, all 70 billion weights in Llama 70B):\n\n- ✅ Maximum model adaptation and knowledge injection\n- ✅ Can fundamentally change model behavior\n- ✅ Best for significant domain shifts or specialized tasks\n- ❌ Requires substantial GPU resources (4-8x more than LoRA)\n- ❌ Produces a full BF16 checkpoint (~140 GB for Llama 70B); peak job disk usage can reach approximately 3× the downloaded base checkpoint size\n- ❌ Longer training time\n\n**LoRA** trains only ~1% of weights by adding thin matrices to existing weights:\n\n- ✅ 75-95% less memory required\n- ✅ Faster training (2-4x speedup)\n- ✅ Produces small adapter files (~100-500MB)\n- ✅ Multiple adapters can share one base model\n- ❌ Limited adaptation capability compared to full fine-tuning\n\n**When to choose Full SFT:**\n\n- Training small models (1B-8B) where resource cost is manageable\n- Need fundamental behavior changes (for example, medical diagnosis, legal reasoning)\n- Injecting substantial new knowledge not in the base model\n\n**When to choose LoRA:** Refer to the [LoRA tutorial](./lora-customization-job) for most use cases, especially with large models (70B+) or limited GPU resources.", + "source_html": "\n\n

Full SFT Customization

\n

Learn how to fine-tune all model weights using supervised fine-tuning (SFT) to customize LLM behavior for your specific tasks.

\n

About

\n

Supervised Fine-Tuning (SFT) customizes model behavior, injects new knowledge, and optimizes performance for specific domains and tasks. Full SFT modifies all model weights during training, providing maximum customization flexibility.

\n

What you can achieve with SFT:

\n
    \n
  • 🎯 Specialize for domains: Fine-tune models on legal texts, medical records, or financial data
  • \n
  • 💡 Inject knowledge: Add new information not present in the base model
  • \n
  • 📈 Improve accuracy: Optimize for specific tasks like sentiment analysis, summarization, or code generation
  • \n
\n

SFT vs LoRA: Understanding the Trade-offs

\n

Full SFT trains all model parameters (for example, all 70 billion weights in Llama 70B):

\n
    \n
  • ✅ Maximum model adaptation and knowledge injection
  • \n
  • ✅ Can fundamentally change model behavior
  • \n
  • ✅ Best for significant domain shifts or specialized tasks
  • \n
  • ❌ Requires substantial GPU resources (4-8x more than LoRA)
  • \n
  • ❌ Produces a full BF16 checkpoint (~140 GB for Llama 70B); peak job disk usage can reach approximately 3× the downloaded base checkpoint size
  • \n
  • ❌ Longer training time
  • \n
\n

LoRA trains only ~1% of weights by adding thin matrices to existing weights:

\n
    \n
  • ✅ 75-95% less memory required
  • \n
  • ✅ Faster training (2-4x speedup)
  • \n
  • ✅ Produces small adapter files (~100-500MB)
  • \n
  • ✅ Multiple adapters can share one base model
  • \n
  • ❌ Limited adaptation capability compared to full fine-tuning
  • \n
\n

When to choose Full SFT:

\n
    \n
  • Training small models (1B-8B) where resource cost is manageable
  • \n
  • Need fundamental behavior changes (for example, medical diagnosis, legal reasoning)
  • \n
  • Injecting substantial new knowledge not in the base model
  • \n
\n

When to choose LoRA: Refer to the LoRA tutorial for most use cases, especially with large models (70B+) or limited GPU resources.

\n" }, { "type": "markdown", - "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)", - "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
\n" + "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **At least one GPU with CUDA 13+**", + "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. At least one GPU with CUDA 13+
  6. \n
\n" }, { "type": "markdown", @@ -85,30 +85,30 @@ }, { "type": "markdown", - "source": "### 4. Secrets Setup\n\nIf you plan to use NGC or HuggingFace models, you will need to configure authentication:\n\n- **NGC models** (`ngc://` URIs): Requires NGC API key\n- **HuggingFace models** (`hf://` URIs): Requires HF token for gated/private models\n\n\nConfigure these as secrets in your platform. Refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md) for detailed instructions.\n\nGet your credentials to access base models:\n- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n- [HuggingFace Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n\n\n---\n\n#### Quick Setup Example\n\nIn this tutorial we are going to work with [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from HuggingFace. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access\n\n**HuggingFace Authentication:**\n- For gated models (Llama, Gemma), you must provide a HuggingFace token via the `token_secret` parameter\n- Get your token from [HuggingFace Settings](https://huggingface.co/settings/tokens) (requires Read access)\n- Accept the model's terms on the HuggingFace model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main)\n- For public models, you can omit the `token_secret` parameter when creating a fileset for model in the next step", - "source_html": "

4. Secrets Setup

\n

If you plan to use NGC or HuggingFace models, you will need to configure authentication:

\n
    \n
  • NGC models (ngc:// URIs): Requires NGC API key
  • \n
  • HuggingFace models (hf:// URIs): Requires HF token for gated/private models
  • \n
\n

Configure these as secrets in your platform. Refer to Managing Secrets for detailed instructions.

\n

Get your credentials to access base models:

\n\n
\n

Quick Setup Example

\n

In this tutorial we are going to work with meta-llama/Llama-3.2-1B-Instruct model from HuggingFace. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the meta-llama/Llama-3.2-1B-Instruct Hugging Face page, request access

\n

HuggingFace Authentication:

\n
    \n
  • For gated models (Llama, Gemma), you must provide a HuggingFace token via the token_secret parameter
  • \n
  • Get your token from HuggingFace Settings (requires Read access)
  • \n
  • Accept the model's terms on the HuggingFace model page before using it. Example: meta-llama/Llama-3.2-1B-Instruct
  • \n
  • For public models, you can omit the token_secret parameter when creating a fileset for model in the next step
  • \n
\n" + "source": "### 4. Secrets Setup\n\nIf you plan to use NGC or Hugging Face models, you will need to configure authentication:\n\n- **NGC models** (`ngc://` URIs): Requires NGC API key\n- **Hugging Face models** (`hf://` URIs): Requires HF token for gated/private models\n\n\nConfigure these as secrets in your platform. Refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md) for detailed instructions.\n\nGet your credentials to access base models:\n- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n- [Hugging Face Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n\n\n---\n\n#### Quick Setup Example\n\nIn this tutorial we are going to work with the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from Hugging Face. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access.\n\n**Hugging Face Authentication:**\n- For gated models (Llama, Gemma), you must provide a Hugging Face token via the `token_secret` parameter\n- Get your token from [Hugging Face Settings](https://huggingface.co/settings/tokens) (requires Read access)\n- Accept the model's terms on the Hugging Face model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main)\n- For public models, you can omit the `token_secret` parameter when creating a fileset for model in the next step", + "source_html": "

4. Secrets Setup

\n

If you plan to use NGC or Hugging Face models, you will need to configure authentication:

\n
    \n
  • NGC models (ngc:// URIs): Requires NGC API key
  • \n
  • Hugging Face models (hf:// URIs): Requires HF token for gated/private models
  • \n
\n

Configure these as secrets in your platform. Refer to Managing Secrets for detailed instructions.

\n

Get your credentials to access base models:

\n\n
\n

Quick Setup Example

\n

In this tutorial we are going to work with the meta-llama/Llama-3.2-1B-Instruct model from Hugging Face. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the meta-llama/Llama-3.2-1B-Instruct Hugging Face page, request access.

\n

Hugging Face Authentication:

\n
    \n
  • For gated models (Llama, Gemma), you must provide a Hugging Face token via the token_secret parameter
  • \n
  • Get your token from Hugging Face Settings (requires Read access)
  • \n
  • Accept the model's terms on the Hugging Face model page before using it. Example: meta-llama/Llama-3.2-1B-Instruct
  • \n
  • For public models, you can omit the token_secret parameter when creating a fileset for model in the next step
  • \n
\n" }, { "type": "code", - "source": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\nHF_TOKEN = os.getenv(\"HF_TOKEN\")\nNGC_API_KEY = os.getenv(\"NGC_API_KEY\")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f\"{label} is not set\")\n try:\n secret = client.secrets.create(\n name=name,\n workspace=\"default\",\n value=value,\n )\n print(f\"Created secret: {name}\")\n return secret\n except ConflictError:\n print(f\"Secret '{name}' already exists, continuing...\")\n return client.secrets.retrieve(name=name, workspace=\"default\")\n\n\n# Create HuggingFace token secret\nhf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\nprint(\"HF_TOKEN secret:\")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret(\"ngc-api-key\", NGC_API_KEY, \"NGC_API_KEY\")", + "source": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set.\n# This tutorial's default model (meta-llama/Llama-3.2-1B-Instruct) is gated and requires HF_TOKEN.\nHF_TOKEN = os.getenv(\"HF_TOKEN\")\nNGC_API_KEY = os.getenv(\"NGC_API_KEY\")\nif not HF_TOKEN:\n raise RuntimeError(\n \"Set HF_TOKEN before running this tutorial. \"\n \"The default model meta-llama/Llama-3.2-1B-Instruct is gated.\"\n )\n\n\ndef create_or_get_secret(name: str, value: str, label: str):\n try:\n secret = client.secrets.create(\n name=name,\n workspace=\"default\",\n value=value,\n )\n print(f\"Created secret: {name}\")\n return secret\n except ConflictError:\n print(f\"Secret '{name}' already exists, continuing...\")\n return client.secrets.retrieve(name=name, workspace=\"default\")\n\n\n# Create Hugging Face token secret\nhf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\nprint(\"HF_TOKEN secret:\")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret(\"ngc-api-key\", NGC_API_KEY, \"NGC_API_KEY\")", "language": "python", - "source_html": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\nHF_TOKEN = os.getenv("HF_TOKEN")\nNGC_API_KEY = os.getenv("NGC_API_KEY")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f"{label} is not set")\n try:\n secret = client.secrets.create(\n name=name,\n workspace="default",\n value=value,\n )\n print(f"Created secret: {name}")\n return secret\n except ConflictError:\n print(f"Secret '{name}' already exists, continuing...")\n return client.secrets.retrieve(name=name, workspace="default")\n\n\n# Create HuggingFace token secret\nhf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN")\nprint("HF_TOKEN secret:")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret("ngc-api-key", NGC_API_KEY, "NGC_API_KEY")\n" + "source_html": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set.\n# This tutorial's default model (meta-llama/Llama-3.2-1B-Instruct) is gated and requires HF_TOKEN.\nHF_TOKEN = os.getenv("HF_TOKEN")\nNGC_API_KEY = os.getenv("NGC_API_KEY")\nif not HF_TOKEN:\n raise RuntimeError(\n "Set HF_TOKEN before running this tutorial. "\n "The default model meta-llama/Llama-3.2-1B-Instruct is gated."\n )\n\n\ndef create_or_get_secret(name: str, value: str, label: str):\n try:\n secret = client.secrets.create(\n name=name,\n workspace="default",\n value=value,\n )\n print(f"Created secret: {name}")\n return secret\n except ConflictError:\n print(f"Secret '{name}' already exists, continuing...")\n return client.secrets.retrieve(name=name, workspace="default")\n\n\n# Create Hugging Face token secret\nhf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN")\nprint("HF_TOKEN secret:")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret("ngc-api-key", NGC_API_KEY, "NGC_API_KEY")\n" }, { "type": "markdown", - "source": "### 5. Create Base Model FileSet and Model Entity\n\nCreate a fileset pointing to [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model in HuggingFace that we will train with SFT. Then create a Model Entity that references this fileset. Model downloading will take place at training time.\n\nNote: for public models, you can omit the `token_secret` parameter when creating a model fileset.", - "source_html": "

5. Create Base Model FileSet and Model Entity

\n

Create a fileset pointing to meta-llama/Llama-3.2-1B-Instruct model in HuggingFace that we will train with SFT. Then create a Model Entity that references this fileset. Model downloading will take place at training time.

\n

Note: for public models, you can omit the token_secret parameter when creating a model fileset.

\n" + "source": "### 5. Create Base Model FileSet and Model Entity\n\nCreate a fileset pointing to the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model in Hugging Face that we will train with SFT. Then create a Model Entity that references this fileset. Model downloading will take place at training time.\n\nThis tutorial's default model is gated, so the fileset includes `token_secret=hf_secret.name`. If you substitute a public model, you can omit `token_secret`.", + "source_html": "

5. Create Base Model FileSet and Model Entity

\n

Create a fileset pointing to the meta-llama/Llama-3.2-1B-Instruct model in Hugging Face that we will train with SFT. Then create a Model Entity that references this fileset. Model downloading will take place at training time.

\n

This tutorial's default model is gated, so the fileset includes token_secret=hf_secret.name. If you substitute a public model, you can omit token_secret.

\n" }, { "type": "code", - "source": "import time\n\n# Create a fileset pointing to the desired HuggingFace model\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\nMODEL_NAME = \"llama-3-2-1b-base\"\n\n# Ensure you have a HuggingFace token secret created\ntry:\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"Llama 3.2 1B base model from HuggingFace\",\n storage=HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\n print(f\"Created base model fileset: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model fileset already exists. Skipping creation.\")\n base_model_fs = client.files.filesets.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n print(f\"Created Model Entity: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model already exists. Updating fileset if different.\")\n base_model = client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n\nprint(f\"\\nBase model fileset: fileset://default/{base_model.name}\")\nprint(\"Base model fileset files list:\")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace=\"default\").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint(\"\\nWaiting for ModelSpec to be populated...\")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds\")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\nprint(f\"ModelSpec populated: {base_model.spec}\")", + "source": "import time\n\n# Create a fileset pointing to the desired Hugging Face model\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\nMODEL_NAME = \"llama-3-2-1b-base\"\n\n# Ensure you have a Hugging Face token secret created\ntry:\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"Llama 3.2 1B base model from Hugging Face\",\n storage=HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n # we use the secret created in the previous step\n token_secret=hf_secret.name,\n ),\n )\n print(f\"Created base model fileset: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model fileset already exists. Skipping creation.\")\n base_model_fs = client.files.filesets.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n print(f\"Created Model Entity: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model already exists. Updating fileset if different.\")\n base_model = client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n\nprint(f\"\\nBase model fileset: fileset://default/{base_model.name}\")\nprint(\"Base model fileset files list:\")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace=\"default\").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint(\"\\nWaiting for ModelSpec to be populated...\")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds\")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\nprint(f\"ModelSpec populated: {base_model.spec}\")", "language": "python", - "source_html": "import time\n\n# Create a fileset pointing to the desired HuggingFace model\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = "meta-llama/Llama-3.2-1B-Instruct"\nMODEL_NAME = "llama-3-2-1b-base"\n\n# Ensure you have a HuggingFace token secret created\ntry:\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="Llama 3.2 1B base model from HuggingFace",\n storage=HuggingfaceStorageConfigParam(\n type="huggingface",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type="model",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\n print(f"Created base model fileset: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model fileset already exists. Skipping creation.")\n base_model_fs = client.files.filesets.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n print(f"Created Model Entity: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model already exists. Updating fileset if different.")\n base_model = client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n\nprint(f"\\nBase model fileset: fileset://default/{base_model.name}")\nprint("Base model fileset files list:")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace="default").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint("\\nWaiting for ModelSpec to be populated...")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\nprint(f"ModelSpec populated: {base_model.spec}")\n" + "source_html": "import time\n\n# Create a fileset pointing to the desired Hugging Face model\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = "meta-llama/Llama-3.2-1B-Instruct"\nMODEL_NAME = "llama-3-2-1b-base"\n\n# Ensure you have a Hugging Face token secret created\ntry:\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="Llama 3.2 1B base model from Hugging Face",\n storage=HuggingfaceStorageConfigParam(\n type="huggingface",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type="model",\n # we use the secret created in the previous step\n token_secret=hf_secret.name,\n ),\n )\n print(f"Created base model fileset: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model fileset already exists. Skipping creation.")\n base_model_fs = client.files.filesets.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n print(f"Created Model Entity: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model already exists. Updating fileset if different.")\n base_model = client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n\nprint(f"\\nBase model fileset: fileset://default/{base_model.name}")\nprint("Base model fileset files list:")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace="default").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint("\\nWaiting for ModelSpec to be populated...")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\nprint(f"ModelSpec populated: {base_model.spec}")\n" }, { "type": "markdown", - "source": "### 6. Create SFT Finetuning Job\nCreate a customization job to fine-tune all model weights using the **Automodel** backend and `AutomodelJobInput`.", - "source_html": "

6. Create SFT Finetuning Job

\n

Create a customization job to fine-tune all model weights using the Automodel backend and AutomodelJobInput.

\n" + "source": "### 6. Create SFT Fine-Tuning Job\nCreate a customization job to fine-tune all model weights using the **Automodel** backend and `AutomodelJobInput`.", + "source_html": "

6. Create SFT Fine-Tuning Job

\n

Create a customization job to fine-tune all model weights using the Automodel backend and AutomodelJobInput.

\n" }, { "type": "markdown", @@ -183,8 +183,8 @@ }, { "type": "markdown", - "source": "#### Evaluation Best Practices\n\n**Manual Evaluation** (Recommended)\n- Test with real-world examples from your use case\n- Compare responses to base model and expected outputs\n- Verify the model exhibits desired behavior changes\n- Check edge cases and error handling\n\n**What to look for:**\n- ✅ Model follows your desired output format\n- ✅ Applies domain knowledge correctly\n- ✅ Maintains general language capabilities\n- ✅ Avoids unwanted behaviors or biases\n- ❌ Doesn't hallucinate facts not in training data\n- ❌ Doesn't produce repetitive or nonsensical outputs\n\n---\n\n## Hyperparameters\n\nFor detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the [Hyperparameter Reference](../manage-customization-jobs/hyperparameters.md).\n\n---\n\n\n## Troubleshooting\n\n**Job fails during model download:**\n- Verify authentication secrets are configured (refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md))\n- For gated HuggingFace models (Llama, Gemma), accept the license on the model page (for example, [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct))\n- Confirm the model fileset uses `token_secret=hf_secret.name` for gated models\n- Check `AutomodelJobInput` references use the `workspace/name` format: `model=f\"default/{MODEL_NAME}\"` and `dataset={\"training\": f\"default/{DATASET_NAME}\"}` (for example, `default/llama-3-2-1b-base`, `default/sft-dataset`)\n- Verify the model entity points at the fileset: `fileset=f\"default/{MODEL_NAME}\"`\n- Check job status: `client.jobs.get_status(name=job.job.name, workspace=\"default\")`\n\n**Job fails with OOM (Out of Memory) error:**\n1. **First try:** Reduce `global_batch_size` from 64 to 32 or 16 in `batch={...}`\n2. **Still OOM:** Keep `micro_batch_size` at 1 (already the minimum in this tutorial)\n3. **Still OOM:** Reduce `max_seq_length` from 2048 to 1024 or 512 in `training={...}`\n4. **Last resort:** Increase `num_gpus_per_node` and `tensor_parallel_size` in `parallelism={...}`\n\n**Loss curves not decreasing (underfitting):**\n- Increase training duration: raise `epochs` from 2 to 3-5 in `schedule={...}`\n- Adjust learning rate: try `1e-4` or `1e-5` instead of the default `5e-5` in `optimizer={...}`\n- Check data quality: Verify formatting, remove duplicates, ensure diversity\n\n**Training loss decreases but validation loss increases (overfitting):**\n- Reduce `epochs` from 2 to 1 in `schedule={...}`\n- Lower `learning_rate` from `5e-5` to `2e-5` or `1e-5` in `optimizer={...}`\n- Increase dataset size and diversity\n- Verify train/validation split has no data leakage\n\n**Model output quality is poor despite good training metrics:**\n- Training metrics optimize for loss, not your actual task—evaluate on real use cases\n- Review data quality, format, and diversity—metrics can be misleading with poor data\n- Try a different base model size or architecture\n- Adjust `learning_rate` and `global_batch_size`\n- Compare to baseline: Test base model to ensure fine-tuning improved performance\n\n**Deployment fails:**\n- Verify output model exists: `client.models.retrieve(name=OUTPUT_NAME, workspace=\"default\")`\n- Check deployment logs: `client.inference.deployments.get_logs(name=deployment.name, workspace=\"default\")`\n- Ensure sufficient GPU resources for `executor_config={\"gpu\": 1, ...}`\n- Verify the deployment config matches this tutorial: `engine=\"vllm\"` with `vllm/vllm-openai:v0.22.1`\n\n\n## Next Steps\n\n- [Monitor training metrics](fine-tune-metrics) in detail\n- [Evaluate your fine-tuned model](../../evaluator/index) using the Evaluator service\n- Learn about [LoRA customization](./lora-customization-job) for resource-efficient fine-tuning", - "source_html": "

Evaluation Best Practices

\n

Manual Evaluation (Recommended)

\n
    \n
  • Test with real-world examples from your use case
  • \n
  • Compare responses to base model and expected outputs
  • \n
  • Verify the model exhibits desired behavior changes
  • \n
  • Check edge cases and error handling
  • \n
\n

What to look for:

\n
    \n
  • ✅ Model follows your desired output format
  • \n
  • ✅ Applies domain knowledge correctly
  • \n
  • ✅ Maintains general language capabilities
  • \n
  • ✅ Avoids unwanted behaviors or biases
  • \n
  • ❌ Doesn't hallucinate facts not in training data
  • \n
  • ❌ Doesn't produce repetitive or nonsensical outputs
  • \n
\n
\n

Hyperparameters

\n

For detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the Hyperparameter Reference.

\n
\n

Troubleshooting

\n

Job fails during model download:

\n
    \n
  • Verify authentication secrets are configured (refer to Managing Secrets)
  • \n
  • For gated HuggingFace models (Llama, Gemma), accept the license on the model page (for example, meta-llama/Llama-3.2-1B-Instruct)
  • \n
  • Confirm the model fileset uses token_secret=hf_secret.name for gated models
  • \n
  • Check AutomodelJobInput references use the workspace/name format: model=f"default/{MODEL_NAME}" and dataset={"training": f"default/{DATASET_NAME}"} (for example, default/llama-3-2-1b-base, default/sft-dataset)
  • \n
  • Verify the model entity points at the fileset: fileset=f"default/{MODEL_NAME}"
  • \n
  • Check job status: client.jobs.get_status(name=job.job.name, workspace="default")
  • \n
\n

Job fails with OOM (Out of Memory) error:

\n
    \n
  1. First try: Reduce global_batch_size from 64 to 32 or 16 in batch={...}
  2. \n
  3. Still OOM: Keep micro_batch_size at 1 (already the minimum in this tutorial)
  4. \n
  5. Still OOM: Reduce max_seq_length from 2048 to 1024 or 512 in training={...}
  6. \n
  7. Last resort: Increase num_gpus_per_node and tensor_parallel_size in parallelism={...}
  8. \n
\n

Loss curves not decreasing (underfitting):

\n
    \n
  • Increase training duration: raise epochs from 2 to 3-5 in schedule={...}
  • \n
  • Adjust learning rate: try 1e-4 or 1e-5 instead of the default 5e-5 in optimizer={...}
  • \n
  • Check data quality: Verify formatting, remove duplicates, ensure diversity
  • \n
\n

Training loss decreases but validation loss increases (overfitting):

\n
    \n
  • Reduce epochs from 2 to 1 in schedule={...}
  • \n
  • Lower learning_rate from 5e-5 to 2e-5 or 1e-5 in optimizer={...}
  • \n
  • Increase dataset size and diversity
  • \n
  • Verify train/validation split has no data leakage
  • \n
\n

Model output quality is poor despite good training metrics:

\n
    \n
  • Training metrics optimize for loss, not your actual task—evaluate on real use cases
  • \n
  • Review data quality, format, and diversity—metrics can be misleading with poor data
  • \n
  • Try a different base model size or architecture
  • \n
  • Adjust learning_rate and global_batch_size
  • \n
  • Compare to baseline: Test base model to ensure fine-tuning improved performance
  • \n
\n

Deployment fails:

\n
    \n
  • Verify output model exists: client.models.retrieve(name=OUTPUT_NAME, workspace="default")
  • \n
  • Check deployment logs: client.inference.deployments.get_logs(name=deployment.name, workspace="default")
  • \n
  • Ensure sufficient GPU resources for executor_config={"gpu": 1, ...}
  • \n
  • Verify the deployment config matches this tutorial: engine="vllm" with vllm/vllm-openai:v0.22.1
  • \n
\n

Next Steps

\n\n" + "source": "#### Evaluation Best Practices\n\n**Manual Evaluation** (Recommended)\n- Test with real-world examples from your use case\n- Compare responses to base model and expected outputs\n- Verify the model exhibits desired behavior changes\n- Check edge cases and error handling\n\n**What to look for:**\n- ✅ Model follows your desired output format\n- ✅ Applies domain knowledge correctly\n- ✅ Maintains general language capabilities\n- ✅ Avoids unwanted behaviors or biases\n- ❌ Doesn't hallucinate facts not in training data\n- ❌ Doesn't produce repetitive or nonsensical outputs\n\n---\n\n## Hyperparameters\n\nFor detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the [Hyperparameter Reference](../manage-customization-jobs/hyperparameters.md).\n\n---\n\n\n## Troubleshooting\n\n**Job fails during model download:**\n- Verify authentication secrets are configured (refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md))\n- For gated Hugging Face models (Llama, Gemma), accept the license on the model page (for example, [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct))\n- Confirm the model fileset uses `token_secret=hf_secret.name` for gated models\n- Check `AutomodelJobInput` references use the `workspace/name` format: `model=f\"default/{MODEL_NAME}\"` and `dataset={\"training\": f\"default/{DATASET_NAME}\"}` (for example, `default/llama-3-2-1b-base`, `default/sft-dataset`)\n- Verify the model entity points at the fileset: `fileset=f\"default/{MODEL_NAME}\"`\n- Check job status: `client.jobs.get_status(name=job.job.name, workspace=\"default\")`\n\n**Job fails with OOM (Out of Memory) error:**\n1. **First try:** Reduce `global_batch_size` from 64 to 32 or 16 in `batch={...}`\n2. **Still OOM:** Keep `micro_batch_size` at 1 (already the minimum in this tutorial)\n3. **Still OOM:** Reduce `max_seq_length` from 2048 to 1024 or 512 in `training={...}`\n4. **Last resort:** Increase `num_gpus_per_node` and `tensor_parallel_size` in `parallelism={...}`\n\n**Loss curves not decreasing (underfitting):**\n- Increase training duration: raise `epochs` from 2 to 3-5 in `schedule={...}`\n- Adjust learning rate: try `1e-4` or `1e-5` instead of the default `5e-5` in `optimizer={...}`\n- Check data quality: Verify formatting, remove duplicates, ensure diversity\n\n**Training loss decreases but validation loss increases (overfitting):**\n- Reduce `epochs` from 2 to 1 in `schedule={...}`\n- Lower `learning_rate` from `5e-5` to `2e-5` or `1e-5` in `optimizer={...}`\n- Increase dataset size and diversity\n- Verify train/validation split has no data leakage\n\n**Model output quality is poor despite good training metrics:**\n- Training metrics optimize for loss, not your actual task—evaluate on real use cases\n- Review data quality, format, and diversity—metrics can be misleading with poor data\n- Try a different base model size or architecture\n- Adjust `learning_rate` and `global_batch_size`\n- Compare to baseline: Test base model to ensure fine-tuning improved performance\n\n**Deployment fails:**\n- Verify output model exists: `client.models.retrieve(name=OUTPUT_NAME, workspace=\"default\")`\n- Check deployment logs: `client.inference.deployments.get_logs(name=deployment.name, workspace=\"default\")`\n- Ensure sufficient GPU resources for `executor_config={\"gpu\": 1, ...}`\n- Verify the deployment config matches this tutorial: `engine=\"vllm\"` with `vllm/vllm-openai:v0.22.1`\n\n\n## Next Steps\n\n- [Monitor training metrics](fine-tune-metrics) in detail\n- [Evaluate your fine-tuned model](../../evaluator/index) using the Evaluator service\n- Learn about [LoRA customization](./lora-customization-job) for resource-efficient fine-tuning", + "source_html": "

Evaluation Best Practices

\n

Manual Evaluation (Recommended)

\n
    \n
  • Test with real-world examples from your use case
  • \n
  • Compare responses to base model and expected outputs
  • \n
  • Verify the model exhibits desired behavior changes
  • \n
  • Check edge cases and error handling
  • \n
\n

What to look for:

\n
    \n
  • ✅ Model follows your desired output format
  • \n
  • ✅ Applies domain knowledge correctly
  • \n
  • ✅ Maintains general language capabilities
  • \n
  • ✅ Avoids unwanted behaviors or biases
  • \n
  • ❌ Doesn't hallucinate facts not in training data
  • \n
  • ❌ Doesn't produce repetitive or nonsensical outputs
  • \n
\n
\n

Hyperparameters

\n

For detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the Hyperparameter Reference.

\n
\n

Troubleshooting

\n

Job fails during model download:

\n
    \n
  • Verify authentication secrets are configured (refer to Managing Secrets)
  • \n
  • For gated Hugging Face models (Llama, Gemma), accept the license on the model page (for example, meta-llama/Llama-3.2-1B-Instruct)
  • \n
  • Confirm the model fileset uses token_secret=hf_secret.name for gated models
  • \n
  • Check AutomodelJobInput references use the workspace/name format: model=f"default/{MODEL_NAME}" and dataset={"training": f"default/{DATASET_NAME}"} (for example, default/llama-3-2-1b-base, default/sft-dataset)
  • \n
  • Verify the model entity points at the fileset: fileset=f"default/{MODEL_NAME}"
  • \n
  • Check job status: client.jobs.get_status(name=job.job.name, workspace="default")
  • \n
\n

Job fails with OOM (Out of Memory) error:

\n
    \n
  1. First try: Reduce global_batch_size from 64 to 32 or 16 in batch={...}
  2. \n
  3. Still OOM: Keep micro_batch_size at 1 (already the minimum in this tutorial)
  4. \n
  5. Still OOM: Reduce max_seq_length from 2048 to 1024 or 512 in training={...}
  6. \n
  7. Last resort: Increase num_gpus_per_node and tensor_parallel_size in parallelism={...}
  8. \n
\n

Loss curves not decreasing (underfitting):

\n
    \n
  • Increase training duration: raise epochs from 2 to 3-5 in schedule={...}
  • \n
  • Adjust learning rate: try 1e-4 or 1e-5 instead of the default 5e-5 in optimizer={...}
  • \n
  • Check data quality: Verify formatting, remove duplicates, ensure diversity
  • \n
\n

Training loss decreases but validation loss increases (overfitting):

\n
    \n
  • Reduce epochs from 2 to 1 in schedule={...}
  • \n
  • Lower learning_rate from 5e-5 to 2e-5 or 1e-5 in optimizer={...}
  • \n
  • Increase dataset size and diversity
  • \n
  • Verify train/validation split has no data leakage
  • \n
\n

Model output quality is poor despite good training metrics:

\n
    \n
  • Training metrics optimize for loss, not your actual task—evaluate on real use cases
  • \n
  • Review data quality, format, and diversity—metrics can be misleading with poor data
  • \n
  • Try a different base model size or architecture
  • \n
  • Adjust learning_rate and global_batch_size
  • \n
  • Compare to baseline: Test base model to ensure fine-tuning improved performance
  • \n
\n

Deployment fails:

\n
    \n
  • Verify output model exists: client.models.retrieve(name=OUTPUT_NAME, workspace="default")
  • \n
  • Check deployment logs: client.inference.deployments.get_logs(name=deployment.name, workspace="default")
  • \n
  • Ensure sufficient GPU resources for executor_config={"gpu": 1, ...}
  • \n
  • Verify the deployment config matches this tutorial: engine="vllm" with vllm/vllm-openai:v0.22.1
  • \n
\n

Next Steps

\n\n" } ] -} +} \ No newline at end of file diff --git a/docs/fern/components/notebooks/sft-customization-job.ts b/docs/fern/components/notebooks/sft-customization-job.ts index 60a7f22ff8..5b804ed7f2 100644 --- a/docs/fern/components/notebooks/sft-customization-job.ts +++ b/docs/fern/components/notebooks/sft-customization-job.ts @@ -7,13 +7,13 @@ export default { cells: [ { "type": "markdown", - "source": "\n\n\n# Full SFT Customization\n\nLearn how to fine-tune all model weights using supervised fine-tuning (SFT) to customize LLM behavior for your specific tasks.\n\n## About\n\nSupervised Fine-Tuning (SFT) customizes model behavior, injects new knowledge, and optimizes performance for specific domains and tasks. Full SFT modifies **all model weights** during training, providing maximum customization flexibility.\n\n**What you can achieve with SFT:**\n\n- 🎯 **Specialize for domains:** Fine-tune models on legal texts, medical records, or financial data\n- 💡 **Inject knowledge:** Add new information not present in the base model\n- 📈 **Improve accuracy:** Optimize for specific tasks like sentiment analysis, summarization, or code generation\n\n### SFT vs LoRA: Understanding the Trade-offs\n\n**Full SFT** trains all model parameters (for example, all 70 billion weights in Llama 70B):\n\n- ✅ Maximum model adaptation and knowledge injection\n- ✅ Can fundamentally change model behavior\n- ✅ Best for significant domain shifts or specialized tasks\n- ❌ Requires substantial GPU resources (4-8x more than LoRA)\n- ❌ Produces full model weights (~140GB for Llama 70B)\n- ❌ Longer training time\n\n**LoRA** trains only ~1% of weights by adding thin matrices to existing weights:\n\n- ✅ 75-95% less memory required\n- ✅ Faster training (2-4x speedup)\n- ✅ Produces small adapter files (~100-500MB)\n- ✅ Multiple adapters can share one base model\n- ❌ Limited adaptation capability compared to full fine-tuning\n\n**When to choose Full SFT:**\n\n- Training small models (1B-8B) where resource cost is manageable\n- Need fundamental behavior changes (for example, medical diagnosis, legal reasoning)\n- Injecting substantial new knowledge not in the base model\n\n**When to choose LoRA:** Refer to the [LoRA tutorial](./lora-customization-job) for most use cases, especially with large models (70B+) or limited GPU resources.", - "source_html": "\n\n

Full SFT Customization

\n

Learn how to fine-tune all model weights using supervised fine-tuning (SFT) to customize LLM behavior for your specific tasks.

\n

About

\n

Supervised Fine-Tuning (SFT) customizes model behavior, injects new knowledge, and optimizes performance for specific domains and tasks. Full SFT modifies all model weights during training, providing maximum customization flexibility.

\n

What you can achieve with SFT:

\n
    \n
  • 🎯 Specialize for domains: Fine-tune models on legal texts, medical records, or financial data
  • \n
  • 💡 Inject knowledge: Add new information not present in the base model
  • \n
  • 📈 Improve accuracy: Optimize for specific tasks like sentiment analysis, summarization, or code generation
  • \n
\n

SFT vs LoRA: Understanding the Trade-offs

\n

Full SFT trains all model parameters (for example, all 70 billion weights in Llama 70B):

\n
    \n
  • ✅ Maximum model adaptation and knowledge injection
  • \n
  • ✅ Can fundamentally change model behavior
  • \n
  • ✅ Best for significant domain shifts or specialized tasks
  • \n
  • ❌ Requires substantial GPU resources (4-8x more than LoRA)
  • \n
  • ❌ Produces full model weights (~140GB for Llama 70B)
  • \n
  • ❌ Longer training time
  • \n
\n

LoRA trains only ~1% of weights by adding thin matrices to existing weights:

\n
    \n
  • ✅ 75-95% less memory required
  • \n
  • ✅ Faster training (2-4x speedup)
  • \n
  • ✅ Produces small adapter files (~100-500MB)
  • \n
  • ✅ Multiple adapters can share one base model
  • \n
  • ❌ Limited adaptation capability compared to full fine-tuning
  • \n
\n

When to choose Full SFT:

\n
    \n
  • Training small models (1B-8B) where resource cost is manageable
  • \n
  • Need fundamental behavior changes (for example, medical diagnosis, legal reasoning)
  • \n
  • Injecting substantial new knowledge not in the base model
  • \n
\n

When to choose LoRA: Refer to the LoRA tutorial for most use cases, especially with large models (70B+) or limited GPU resources.

\n" + "source": "\n\n\n# Full SFT Customization\n\nLearn how to fine-tune all model weights using supervised fine-tuning (SFT) to customize LLM behavior for your specific tasks.\n\n## About\n\nSupervised Fine-Tuning (SFT) customizes model behavior, injects new knowledge, and optimizes performance for specific domains and tasks. Full SFT modifies **all model weights** during training, providing maximum customization flexibility.\n\n**What you can achieve with SFT:**\n\n- 🎯 **Specialize for domains:** Fine-tune models on legal texts, medical records, or financial data\n- 💡 **Inject knowledge:** Add new information not present in the base model\n- 📈 **Improve accuracy:** Optimize for specific tasks like sentiment analysis, summarization, or code generation\n\n### SFT vs LoRA: Understanding the Trade-offs\n\n**Full SFT** trains all model parameters (for example, all 70 billion weights in Llama 70B):\n\n- ✅ Maximum model adaptation and knowledge injection\n- ✅ Can fundamentally change model behavior\n- ✅ Best for significant domain shifts or specialized tasks\n- ❌ Requires substantial GPU resources (4-8x more than LoRA)\n- ❌ Produces a full BF16 checkpoint (~140 GB for Llama 70B); peak job disk usage can reach approximately 3× the downloaded base checkpoint size\n- ❌ Longer training time\n\n**LoRA** trains only ~1% of weights by adding thin matrices to existing weights:\n\n- ✅ 75-95% less memory required\n- ✅ Faster training (2-4x speedup)\n- ✅ Produces small adapter files (~100-500MB)\n- ✅ Multiple adapters can share one base model\n- ❌ Limited adaptation capability compared to full fine-tuning\n\n**When to choose Full SFT:**\n\n- Training small models (1B-8B) where resource cost is manageable\n- Need fundamental behavior changes (for example, medical diagnosis, legal reasoning)\n- Injecting substantial new knowledge not in the base model\n\n**When to choose LoRA:** Refer to the [LoRA tutorial](./lora-customization-job) for most use cases, especially with large models (70B+) or limited GPU resources.", + "source_html": "\n\n

Full SFT Customization

\n

Learn how to fine-tune all model weights using supervised fine-tuning (SFT) to customize LLM behavior for your specific tasks.

\n

About

\n

Supervised Fine-Tuning (SFT) customizes model behavior, injects new knowledge, and optimizes performance for specific domains and tasks. Full SFT modifies all model weights during training, providing maximum customization flexibility.

\n

What you can achieve with SFT:

\n
    \n
  • 🎯 Specialize for domains: Fine-tune models on legal texts, medical records, or financial data
  • \n
  • 💡 Inject knowledge: Add new information not present in the base model
  • \n
  • 📈 Improve accuracy: Optimize for specific tasks like sentiment analysis, summarization, or code generation
  • \n
\n

SFT vs LoRA: Understanding the Trade-offs

\n

Full SFT trains all model parameters (for example, all 70 billion weights in Llama 70B):

\n
    \n
  • ✅ Maximum model adaptation and knowledge injection
  • \n
  • ✅ Can fundamentally change model behavior
  • \n
  • ✅ Best for significant domain shifts or specialized tasks
  • \n
  • ❌ Requires substantial GPU resources (4-8x more than LoRA)
  • \n
  • ❌ Produces a full BF16 checkpoint (~140 GB for Llama 70B); peak job disk usage can reach approximately 3× the downloaded base checkpoint size
  • \n
  • ❌ Longer training time
  • \n
\n

LoRA trains only ~1% of weights by adding thin matrices to existing weights:

\n
    \n
  • ✅ 75-95% less memory required
  • \n
  • ✅ Faster training (2-4x speedup)
  • \n
  • ✅ Produces small adapter files (~100-500MB)
  • \n
  • ✅ Multiple adapters can share one base model
  • \n
  • ❌ Limited adaptation capability compared to full fine-tuning
  • \n
\n

When to choose Full SFT:

\n
    \n
  • Training small models (1B-8B) where resource cost is manageable
  • \n
  • Need fundamental behavior changes (for example, medical diagnosis, legal reasoning)
  • \n
  • Injecting substantial new knowledge not in the base model
  • \n
\n

When to choose LoRA: Refer to the LoRA tutorial for most use cases, especially with large models (70B+) or limited GPU resources.

\n" }, { "type": "markdown", - "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)", - "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
\n" + "source": "## Prerequisites\n\nBefore starting this tutorial, ensure you have:\n\n1. **Completed the [Quickstart](../../get-started/quickstart.md)** to install and deploy NeMo Platform locally\n2. **Installed the Python SDK** (PyPI wrapper: `pip install \"nemo-platform[all]\"`; source checkout: run `make bootstrap` from the repository root)\n3. **At least one GPU with CUDA 13+**", + "source_html": "

Prerequisites

\n

Before starting this tutorial, ensure you have:

\n
    \n
  1. Completed the Quickstart to install and deploy NeMo Platform locally
  2. \n
  3. Installed the Python SDK (PyPI wrapper: pip install "nemo-platform[all]"; source checkout: run make bootstrap from the repository root)
  4. \n
  5. At least one GPU with CUDA 13+
  6. \n
\n" }, { "type": "markdown", @@ -90,30 +90,30 @@ export default { cells: [ }, { "type": "markdown", - "source": "### 4. Secrets Setup\n\nIf you plan to use NGC or HuggingFace models, you will need to configure authentication:\n\n- **NGC models** (`ngc://` URIs): Requires NGC API key\n- **HuggingFace models** (`hf://` URIs): Requires HF token for gated/private models\n\n\nConfigure these as secrets in your platform. Refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md) for detailed instructions.\n\nGet your credentials to access base models:\n- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n- [HuggingFace Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n\n\n---\n\n#### Quick Setup Example\n\nIn this tutorial we are going to work with [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from HuggingFace. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access\n\n**HuggingFace Authentication:**\n- For gated models (Llama, Gemma), you must provide a HuggingFace token via the `token_secret` parameter\n- Get your token from [HuggingFace Settings](https://huggingface.co/settings/tokens) (requires Read access)\n- Accept the model's terms on the HuggingFace model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main)\n- For public models, you can omit the `token_secret` parameter when creating a fileset for model in the next step", - "source_html": "

4. Secrets Setup

\n

If you plan to use NGC or HuggingFace models, you will need to configure authentication:

\n
    \n
  • NGC models (ngc:// URIs): Requires NGC API key
  • \n
  • HuggingFace models (hf:// URIs): Requires HF token for gated/private models
  • \n
\n

Configure these as secrets in your platform. Refer to Managing Secrets for detailed instructions.

\n

Get your credentials to access base models:

\n\n
\n

Quick Setup Example

\n

In this tutorial we are going to work with meta-llama/Llama-3.2-1B-Instruct model from HuggingFace. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the meta-llama/Llama-3.2-1B-Instruct Hugging Face page, request access

\n

HuggingFace Authentication:

\n
    \n
  • For gated models (Llama, Gemma), you must provide a HuggingFace token via the token_secret parameter
  • \n
  • Get your token from HuggingFace Settings (requires Read access)
  • \n
  • Accept the model's terms on the HuggingFace model page before using it. Example: meta-llama/Llama-3.2-1B-Instruct
  • \n
  • For public models, you can omit the token_secret parameter when creating a fileset for model in the next step
  • \n
\n" + "source": "### 4. Secrets Setup\n\nIf you plan to use NGC or Hugging Face models, you will need to configure authentication:\n\n- **NGC models** (`ngc://` URIs): Requires NGC API key\n- **Hugging Face models** (`hf://` URIs): Requires HF token for gated/private models\n\n\nConfigure these as secrets in your platform. Refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md) for detailed instructions.\n\nGet your credentials to access base models:\n- [NGC API Key](https://ngc.nvidia.com/) (Setup → Generate API Key)\n- [Hugging Face Token](https://huggingface.co/settings/tokens) (Create token with Read access)\n\n\n---\n\n#### Quick Setup Example\n\nIn this tutorial we are going to work with the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model from Hugging Face. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) Hugging Face page, request access.\n\n**Hugging Face Authentication:**\n- For gated models (Llama, Gemma), you must provide a Hugging Face token via the `token_secret` parameter\n- Get your token from [Hugging Face Settings](https://huggingface.co/settings/tokens) (requires Read access)\n- Accept the model's terms on the Hugging Face model page before using it. Example: [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main)\n- For public models, you can omit the `token_secret` parameter when creating a fileset for model in the next step", + "source_html": "

4. Secrets Setup

\n

If you plan to use NGC or Hugging Face models, you will need to configure authentication:

\n
    \n
  • NGC models (ngc:// URIs): Requires NGC API key
  • \n
  • Hugging Face models (hf:// URIs): Requires HF token for gated/private models
  • \n
\n

Configure these as secrets in your platform. Refer to Managing Secrets for detailed instructions.

\n

Get your credentials to access base models:

\n\n
\n

Quick Setup Example

\n

In this tutorial we are going to work with the meta-llama/Llama-3.2-1B-Instruct model from Hugging Face. Ensure that you have sufficient permissions to download the model. If you cannot access the files on the meta-llama/Llama-3.2-1B-Instruct Hugging Face page, request access.

\n

Hugging Face Authentication:

\n
    \n
  • For gated models (Llama, Gemma), you must provide a Hugging Face token via the token_secret parameter
  • \n
  • Get your token from Hugging Face Settings (requires Read access)
  • \n
  • Accept the model's terms on the Hugging Face model page before using it. Example: meta-llama/Llama-3.2-1B-Instruct
  • \n
  • For public models, you can omit the token_secret parameter when creating a fileset for model in the next step
  • \n
\n" }, { "type": "code", - "source": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\nHF_TOKEN = os.getenv(\"HF_TOKEN\")\nNGC_API_KEY = os.getenv(\"NGC_API_KEY\")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f\"{label} is not set\")\n try:\n secret = client.secrets.create(\n name=name,\n workspace=\"default\",\n value=value,\n )\n print(f\"Created secret: {name}\")\n return secret\n except ConflictError:\n print(f\"Secret '{name}' already exists, continuing...\")\n return client.secrets.retrieve(name=name, workspace=\"default\")\n\n\n# Create HuggingFace token secret\nhf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\nprint(\"HF_TOKEN secret:\")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret(\"ngc-api-key\", NGC_API_KEY, \"NGC_API_KEY\")", + "source": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set.\n# This tutorial's default model (meta-llama/Llama-3.2-1B-Instruct) is gated and requires HF_TOKEN.\nHF_TOKEN = os.getenv(\"HF_TOKEN\")\nNGC_API_KEY = os.getenv(\"NGC_API_KEY\")\nif not HF_TOKEN:\n raise RuntimeError(\n \"Set HF_TOKEN before running this tutorial. \"\n \"The default model meta-llama/Llama-3.2-1B-Instruct is gated.\"\n )\n\n\ndef create_or_get_secret(name: str, value: str, label: str):\n try:\n secret = client.secrets.create(\n name=name,\n workspace=\"default\",\n value=value,\n )\n print(f\"Created secret: {name}\")\n return secret\n except ConflictError:\n print(f\"Secret '{name}' already exists, continuing...\")\n return client.secrets.retrieve(name=name, workspace=\"default\")\n\n\n# Create Hugging Face token secret\nhf_secret = create_or_get_secret(\"hf-token\", HF_TOKEN, \"HF_TOKEN\")\nprint(\"HF_TOKEN secret:\")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret(\"ngc-api-key\", NGC_API_KEY, \"NGC_API_KEY\")", "language": "python", - "source_html": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set\nHF_TOKEN = os.getenv("HF_TOKEN")\nNGC_API_KEY = os.getenv("NGC_API_KEY")\n\n\ndef create_or_get_secret(name: str, value: str | None, label: str):\n if not value:\n raise ValueError(f"{label} is not set")\n try:\n secret = client.secrets.create(\n name=name,\n workspace="default",\n value=value,\n )\n print(f"Created secret: {name}")\n return secret\n except ConflictError:\n print(f"Secret '{name}' already exists, continuing...")\n return client.secrets.retrieve(name=name, workspace="default")\n\n\n# Create HuggingFace token secret\nhf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN")\nprint("HF_TOKEN secret:")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret("ngc-api-key", NGC_API_KEY, "NGC_API_KEY")\n" + "source_html": "# Export the HF_TOKEN and NGC_API_KEY environment variables if they are not already set.\n# This tutorial's default model (meta-llama/Llama-3.2-1B-Instruct) is gated and requires HF_TOKEN.\nHF_TOKEN = os.getenv("HF_TOKEN")\nNGC_API_KEY = os.getenv("NGC_API_KEY")\nif not HF_TOKEN:\n raise RuntimeError(\n "Set HF_TOKEN before running this tutorial. "\n "The default model meta-llama/Llama-3.2-1B-Instruct is gated."\n )\n\n\ndef create_or_get_secret(name: str, value: str, label: str):\n try:\n secret = client.secrets.create(\n name=name,\n workspace="default",\n value=value,\n )\n print(f"Created secret: {name}")\n return secret\n except ConflictError:\n print(f"Secret '{name}' already exists, continuing...")\n return client.secrets.retrieve(name=name, workspace="default")\n\n\n# Create Hugging Face token secret\nhf_secret = create_or_get_secret("hf-token", HF_TOKEN, "HF_TOKEN")\nprint("HF_TOKEN secret:")\nprint(hf_secret.model_dump_json(indent=2))\n\n# Create NGC API key secret\n# Uncomment the line below if you have NGC API Key and want to finetune NGC models\n# ngc_api_key = create_or_get_secret("ngc-api-key", NGC_API_KEY, "NGC_API_KEY")\n" }, { "type": "markdown", - "source": "### 5. Create Base Model FileSet and Model Entity\n\nCreate a fileset pointing to [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model in HuggingFace that we will train with SFT. Then create a Model Entity that references this fileset. Model downloading will take place at training time.\n\nNote: for public models, you can omit the `token_secret` parameter when creating a model fileset.", - "source_html": "

5. Create Base Model FileSet and Model Entity

\n

Create a fileset pointing to meta-llama/Llama-3.2-1B-Instruct model in HuggingFace that we will train with SFT. Then create a Model Entity that references this fileset. Model downloading will take place at training time.

\n

Note: for public models, you can omit the token_secret parameter when creating a model fileset.

\n" + "source": "### 5. Create Base Model FileSet and Model Entity\n\nCreate a fileset pointing to the [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct/tree/main) model in Hugging Face that we will train with SFT. Then create a Model Entity that references this fileset. Model downloading will take place at training time.\n\nThis tutorial's default model is gated, so the fileset includes `token_secret=hf_secret.name`. If you substitute a public model, you can omit `token_secret`.", + "source_html": "

5. Create Base Model FileSet and Model Entity

\n

Create a fileset pointing to the meta-llama/Llama-3.2-1B-Instruct model in Hugging Face that we will train with SFT. Then create a Model Entity that references this fileset. Model downloading will take place at training time.

\n

This tutorial's default model is gated, so the fileset includes token_secret=hf_secret.name. If you substitute a public model, you can omit token_secret.

\n" }, { "type": "code", - "source": "import time\n\n# Create a fileset pointing to the desired HuggingFace model\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\nMODEL_NAME = \"llama-3-2-1b-base\"\n\n# Ensure you have a HuggingFace token secret created\ntry:\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"Llama 3.2 1B base model from HuggingFace\",\n storage=HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\n print(f\"Created base model fileset: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model fileset already exists. Skipping creation.\")\n base_model_fs = client.files.filesets.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n print(f\"Created Model Entity: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model already exists. Updating fileset if different.\")\n base_model = client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n\nprint(f\"\\nBase model fileset: fileset://default/{base_model.name}\")\nprint(\"Base model fileset files list:\")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace=\"default\").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint(\"\\nWaiting for ModelSpec to be populated...\")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds\")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\nprint(f\"ModelSpec populated: {base_model.spec}\")", + "source": "import time\n\n# Create a fileset pointing to the desired Hugging Face model\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = \"meta-llama/Llama-3.2-1B-Instruct\"\nMODEL_NAME = \"llama-3-2-1b-base\"\n\n# Ensure you have a Hugging Face token secret created\ntry:\n base_model_fs = client.files.filesets.create(\n workspace=\"default\",\n name=MODEL_NAME,\n description=\"Llama 3.2 1B base model from Hugging Face\",\n storage=HuggingfaceStorageConfigParam(\n type=\"huggingface\",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type=\"model\",\n # we use the secret created in the previous step\n token_secret=hf_secret.name,\n ),\n )\n print(f\"Created base model fileset: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model fileset already exists. Skipping creation.\")\n base_model_fs = client.files.filesets.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n print(f\"Created Model Entity: {MODEL_NAME}\")\nexcept ConflictError:\n print(f\"Base model already exists. Updating fileset if different.\")\n base_model = client.models.update(\n workspace=\"default\",\n name=MODEL_NAME,\n fileset=f\"default/{MODEL_NAME}\",\n )\n\nprint(f\"\\nBase model fileset: fileset://default/{base_model.name}\")\nprint(\"Base model fileset files list:\")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace=\"default\").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint(\"\\nWaiting for ModelSpec to be populated...\")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f\"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds\")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace=\"default\",\n name=MODEL_NAME,\n )\n\nprint(f\"ModelSpec populated: {base_model.spec}\")", "language": "python", - "source_html": "import time\n\n# Create a fileset pointing to the desired HuggingFace model\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = "meta-llama/Llama-3.2-1B-Instruct"\nMODEL_NAME = "llama-3-2-1b-base"\n\n# Ensure you have a HuggingFace token secret created\ntry:\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="Llama 3.2 1B base model from HuggingFace",\n storage=HuggingfaceStorageConfigParam(\n type="huggingface",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type="model",\n # we use the secret created in the previous step\n token_secret=hf_secret.name\n )\n )\n print(f"Created base model fileset: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model fileset already exists. Skipping creation.")\n base_model_fs = client.files.filesets.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n print(f"Created Model Entity: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model already exists. Updating fileset if different.")\n base_model = client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n\nprint(f"\\nBase model fileset: fileset://default/{base_model.name}")\nprint("Base model fileset files list:")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace="default").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint("\\nWaiting for ModelSpec to be populated...")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\nprint(f"ModelSpec populated: {base_model.spec}")\n" + "source_html": "import time\n\n# Create a fileset pointing to the desired Hugging Face model\nfrom nemo_platform.types.files import HuggingfaceStorageConfigParam\n\nHF_REPO_ID = "meta-llama/Llama-3.2-1B-Instruct"\nMODEL_NAME = "llama-3-2-1b-base"\n\n# Ensure you have a Hugging Face token secret created\ntry:\n base_model_fs = client.files.filesets.create(\n workspace="default",\n name=MODEL_NAME,\n description="Llama 3.2 1B base model from Hugging Face",\n storage=HuggingfaceStorageConfigParam(\n type="huggingface",\n # repo_id is the full model name from Hugging Face\n repo_id=HF_REPO_ID,\n repo_type="model",\n # we use the secret created in the previous step\n token_secret=hf_secret.name,\n ),\n )\n print(f"Created base model fileset: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model fileset already exists. Skipping creation.")\n base_model_fs = client.files.filesets.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\n# Create the Model Entity representation.\ntry:\n base_model = client.models.create(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n print(f"Created Model Entity: {MODEL_NAME}")\nexcept ConflictError:\n print(f"Base model already exists. Updating fileset if different.")\n base_model = client.models.update(\n workspace="default",\n name=MODEL_NAME,\n fileset=f"default/{MODEL_NAME}",\n )\n\nprint(f"\\nBase model fileset: fileset://default/{base_model.name}")\nprint("Base model fileset files list:")\nprint(json.dumps([f.model_dump() for f in client.files.list(fileset=MODEL_NAME, workspace="default").data], indent=2))\n\n# Wait for ModelSpec to be populated from the checkpoint\nprint("\\nWaiting for ModelSpec to be populated...")\nSPEC_TIMEOUT_SECONDS = 120\nspec_start = time.time()\nwhile not base_model.spec:\n if time.time() - spec_start > SPEC_TIMEOUT_SECONDS:\n raise TimeoutError(f"ModelSpec not populated within {SPEC_TIMEOUT_SECONDS} seconds")\n time.sleep(2)\n base_model = client.models.retrieve(\n workspace="default",\n name=MODEL_NAME,\n )\n\nprint(f"ModelSpec populated: {base_model.spec}")\n" }, { "type": "markdown", - "source": "### 6. Create SFT Finetuning Job\nCreate a customization job to fine-tune all model weights using the **Automodel** backend and `AutomodelJobInput`.", - "source_html": "

6. Create SFT Finetuning Job

\n

Create a customization job to fine-tune all model weights using the Automodel backend and AutomodelJobInput.

\n" + "source": "### 6. Create SFT Fine-Tuning Job\nCreate a customization job to fine-tune all model weights using the **Automodel** backend and `AutomodelJobInput`.", + "source_html": "

6. Create SFT Fine-Tuning Job

\n

Create a customization job to fine-tune all model weights using the Automodel backend and AutomodelJobInput.

\n" }, { "type": "markdown", @@ -188,7 +188,7 @@ export default { cells: [ }, { "type": "markdown", - "source": "#### Evaluation Best Practices\n\n**Manual Evaluation** (Recommended)\n- Test with real-world examples from your use case\n- Compare responses to base model and expected outputs\n- Verify the model exhibits desired behavior changes\n- Check edge cases and error handling\n\n**What to look for:**\n- ✅ Model follows your desired output format\n- ✅ Applies domain knowledge correctly\n- ✅ Maintains general language capabilities\n- ✅ Avoids unwanted behaviors or biases\n- ❌ Doesn't hallucinate facts not in training data\n- ❌ Doesn't produce repetitive or nonsensical outputs\n\n---\n\n## Hyperparameters\n\nFor detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the [Hyperparameter Reference](../manage-customization-jobs/hyperparameters.md).\n\n---\n\n\n## Troubleshooting\n\n**Job fails during model download:**\n- Verify authentication secrets are configured (refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md))\n- For gated HuggingFace models (Llama, Gemma), accept the license on the model page (for example, [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct))\n- Confirm the model fileset uses `token_secret=hf_secret.name` for gated models\n- Check `AutomodelJobInput` references use the `workspace/name` format: `model=f\"default/{MODEL_NAME}\"` and `dataset={\"training\": f\"default/{DATASET_NAME}\"}` (for example, `default/llama-3-2-1b-base`, `default/sft-dataset`)\n- Verify the model entity points at the fileset: `fileset=f\"default/{MODEL_NAME}\"`\n- Check job status: `client.jobs.get_status(name=job.job.name, workspace=\"default\")`\n\n**Job fails with OOM (Out of Memory) error:**\n1. **First try:** Reduce `global_batch_size` from 64 to 32 or 16 in `batch={...}`\n2. **Still OOM:** Keep `micro_batch_size` at 1 (already the minimum in this tutorial)\n3. **Still OOM:** Reduce `max_seq_length` from 2048 to 1024 or 512 in `training={...}`\n4. **Last resort:** Increase `num_gpus_per_node` and `tensor_parallel_size` in `parallelism={...}`\n\n**Loss curves not decreasing (underfitting):**\n- Increase training duration: raise `epochs` from 2 to 3-5 in `schedule={...}`\n- Adjust learning rate: try `1e-4` or `1e-5` instead of the default `5e-5` in `optimizer={...}`\n- Check data quality: Verify formatting, remove duplicates, ensure diversity\n\n**Training loss decreases but validation loss increases (overfitting):**\n- Reduce `epochs` from 2 to 1 in `schedule={...}`\n- Lower `learning_rate` from `5e-5` to `2e-5` or `1e-5` in `optimizer={...}`\n- Increase dataset size and diversity\n- Verify train/validation split has no data leakage\n\n**Model output quality is poor despite good training metrics:**\n- Training metrics optimize for loss, not your actual task—evaluate on real use cases\n- Review data quality, format, and diversity—metrics can be misleading with poor data\n- Try a different base model size or architecture\n- Adjust `learning_rate` and `global_batch_size`\n- Compare to baseline: Test base model to ensure fine-tuning improved performance\n\n**Deployment fails:**\n- Verify output model exists: `client.models.retrieve(name=OUTPUT_NAME, workspace=\"default\")`\n- Check deployment logs: `client.inference.deployments.get_logs(name=deployment.name, workspace=\"default\")`\n- Ensure sufficient GPU resources for `executor_config={\"gpu\": 1, ...}`\n- Verify the deployment config matches this tutorial: `engine=\"vllm\"` with `vllm/vllm-openai:v0.22.1`\n\n\n## Next Steps\n\n- [Monitor training metrics](fine-tune-metrics) in detail\n- [Evaluate your fine-tuned model](../../evaluator/index) using the Evaluator service\n- Learn about [LoRA customization](./lora-customization-job) for resource-efficient fine-tuning", - "source_html": "

Evaluation Best Practices

\n

Manual Evaluation (Recommended)

\n
    \n
  • Test with real-world examples from your use case
  • \n
  • Compare responses to base model and expected outputs
  • \n
  • Verify the model exhibits desired behavior changes
  • \n
  • Check edge cases and error handling
  • \n
\n

What to look for:

\n
    \n
  • ✅ Model follows your desired output format
  • \n
  • ✅ Applies domain knowledge correctly
  • \n
  • ✅ Maintains general language capabilities
  • \n
  • ✅ Avoids unwanted behaviors or biases
  • \n
  • ❌ Doesn't hallucinate facts not in training data
  • \n
  • ❌ Doesn't produce repetitive or nonsensical outputs
  • \n
\n
\n

Hyperparameters

\n

For detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the Hyperparameter Reference.

\n
\n

Troubleshooting

\n

Job fails during model download:

\n
    \n
  • Verify authentication secrets are configured (refer to Managing Secrets)
  • \n
  • For gated HuggingFace models (Llama, Gemma), accept the license on the model page (for example, meta-llama/Llama-3.2-1B-Instruct)
  • \n
  • Confirm the model fileset uses token_secret=hf_secret.name for gated models
  • \n
  • Check AutomodelJobInput references use the workspace/name format: model=f"default/{MODEL_NAME}" and dataset={"training": f"default/{DATASET_NAME}"} (for example, default/llama-3-2-1b-base, default/sft-dataset)
  • \n
  • Verify the model entity points at the fileset: fileset=f"default/{MODEL_NAME}"
  • \n
  • Check job status: client.jobs.get_status(name=job.job.name, workspace="default")
  • \n
\n

Job fails with OOM (Out of Memory) error:

\n
    \n
  1. First try: Reduce global_batch_size from 64 to 32 or 16 in batch={...}
  2. \n
  3. Still OOM: Keep micro_batch_size at 1 (already the minimum in this tutorial)
  4. \n
  5. Still OOM: Reduce max_seq_length from 2048 to 1024 or 512 in training={...}
  6. \n
  7. Last resort: Increase num_gpus_per_node and tensor_parallel_size in parallelism={...}
  8. \n
\n

Loss curves not decreasing (underfitting):

\n
    \n
  • Increase training duration: raise epochs from 2 to 3-5 in schedule={...}
  • \n
  • Adjust learning rate: try 1e-4 or 1e-5 instead of the default 5e-5 in optimizer={...}
  • \n
  • Check data quality: Verify formatting, remove duplicates, ensure diversity
  • \n
\n

Training loss decreases but validation loss increases (overfitting):

\n
    \n
  • Reduce epochs from 2 to 1 in schedule={...}
  • \n
  • Lower learning_rate from 5e-5 to 2e-5 or 1e-5 in optimizer={...}
  • \n
  • Increase dataset size and diversity
  • \n
  • Verify train/validation split has no data leakage
  • \n
\n

Model output quality is poor despite good training metrics:

\n
    \n
  • Training metrics optimize for loss, not your actual task—evaluate on real use cases
  • \n
  • Review data quality, format, and diversity—metrics can be misleading with poor data
  • \n
  • Try a different base model size or architecture
  • \n
  • Adjust learning_rate and global_batch_size
  • \n
  • Compare to baseline: Test base model to ensure fine-tuning improved performance
  • \n
\n

Deployment fails:

\n
    \n
  • Verify output model exists: client.models.retrieve(name=OUTPUT_NAME, workspace="default")
  • \n
  • Check deployment logs: client.inference.deployments.get_logs(name=deployment.name, workspace="default")
  • \n
  • Ensure sufficient GPU resources for executor_config={"gpu": 1, ...}
  • \n
  • Verify the deployment config matches this tutorial: engine="vllm" with vllm/vllm-openai:v0.22.1
  • \n
\n

Next Steps

\n\n" + "source": "#### Evaluation Best Practices\n\n**Manual Evaluation** (Recommended)\n- Test with real-world examples from your use case\n- Compare responses to base model and expected outputs\n- Verify the model exhibits desired behavior changes\n- Check edge cases and error handling\n\n**What to look for:**\n- ✅ Model follows your desired output format\n- ✅ Applies domain knowledge correctly\n- ✅ Maintains general language capabilities\n- ✅ Avoids unwanted behaviors or biases\n- ❌ Doesn't hallucinate facts not in training data\n- ❌ Doesn't produce repetitive or nonsensical outputs\n\n---\n\n## Hyperparameters\n\nFor detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the [Hyperparameter Reference](../manage-customization-jobs/hyperparameters.md).\n\n---\n\n\n## Troubleshooting\n\n**Job fails during model download:**\n- Verify authentication secrets are configured (refer to [Managing Secrets](../../get-started/concepts/manage-secrets.md))\n- For gated Hugging Face models (Llama, Gemma), accept the license on the model page (for example, [meta-llama/Llama-3.2-1B-Instruct](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct))\n- Confirm the model fileset uses `token_secret=hf_secret.name` for gated models\n- Check `AutomodelJobInput` references use the `workspace/name` format: `model=f\"default/{MODEL_NAME}\"` and `dataset={\"training\": f\"default/{DATASET_NAME}\"}` (for example, `default/llama-3-2-1b-base`, `default/sft-dataset`)\n- Verify the model entity points at the fileset: `fileset=f\"default/{MODEL_NAME}\"`\n- Check job status: `client.jobs.get_status(name=job.job.name, workspace=\"default\")`\n\n**Job fails with OOM (Out of Memory) error:**\n1. **First try:** Reduce `global_batch_size` from 64 to 32 or 16 in `batch={...}`\n2. **Still OOM:** Keep `micro_batch_size` at 1 (already the minimum in this tutorial)\n3. **Still OOM:** Reduce `max_seq_length` from 2048 to 1024 or 512 in `training={...}`\n4. **Last resort:** Increase `num_gpus_per_node` and `tensor_parallel_size` in `parallelism={...}`\n\n**Loss curves not decreasing (underfitting):**\n- Increase training duration: raise `epochs` from 2 to 3-5 in `schedule={...}`\n- Adjust learning rate: try `1e-4` or `1e-5` instead of the default `5e-5` in `optimizer={...}`\n- Check data quality: Verify formatting, remove duplicates, ensure diversity\n\n**Training loss decreases but validation loss increases (overfitting):**\n- Reduce `epochs` from 2 to 1 in `schedule={...}`\n- Lower `learning_rate` from `5e-5` to `2e-5` or `1e-5` in `optimizer={...}`\n- Increase dataset size and diversity\n- Verify train/validation split has no data leakage\n\n**Model output quality is poor despite good training metrics:**\n- Training metrics optimize for loss, not your actual task—evaluate on real use cases\n- Review data quality, format, and diversity—metrics can be misleading with poor data\n- Try a different base model size or architecture\n- Adjust `learning_rate` and `global_batch_size`\n- Compare to baseline: Test base model to ensure fine-tuning improved performance\n\n**Deployment fails:**\n- Verify output model exists: `client.models.retrieve(name=OUTPUT_NAME, workspace=\"default\")`\n- Check deployment logs: `client.inference.deployments.get_logs(name=deployment.name, workspace=\"default\")`\n- Ensure sufficient GPU resources for `executor_config={\"gpu\": 1, ...}`\n- Verify the deployment config matches this tutorial: `engine=\"vllm\"` with `vllm/vllm-openai:v0.22.1`\n\n\n## Next Steps\n\n- [Monitor training metrics](fine-tune-metrics) in detail\n- [Evaluate your fine-tuned model](../../evaluator/index) using the Evaluator service\n- Learn about [LoRA customization](./lora-customization-job) for resource-efficient fine-tuning", + "source_html": "

Evaluation Best Practices

\n

Manual Evaluation (Recommended)

\n
    \n
  • Test with real-world examples from your use case
  • \n
  • Compare responses to base model and expected outputs
  • \n
  • Verify the model exhibits desired behavior changes
  • \n
  • Check edge cases and error handling
  • \n
\n

What to look for:

\n
    \n
  • ✅ Model follows your desired output format
  • \n
  • ✅ Applies domain knowledge correctly
  • \n
  • ✅ Maintains general language capabilities
  • \n
  • ✅ Avoids unwanted behaviors or biases
  • \n
  • ❌ Doesn't hallucinate facts not in training data
  • \n
  • ❌ Doesn't produce repetitive or nonsensical outputs
  • \n
\n
\n

Hyperparameters

\n

For detailed information on all available hyperparameters, recommended values, and tuning guidance, refer to the Hyperparameter Reference.

\n
\n

Troubleshooting

\n

Job fails during model download:

\n
    \n
  • Verify authentication secrets are configured (refer to Managing Secrets)
  • \n
  • For gated Hugging Face models (Llama, Gemma), accept the license on the model page (for example, meta-llama/Llama-3.2-1B-Instruct)
  • \n
  • Confirm the model fileset uses token_secret=hf_secret.name for gated models
  • \n
  • Check AutomodelJobInput references use the workspace/name format: model=f"default/{MODEL_NAME}" and dataset={"training": f"default/{DATASET_NAME}"} (for example, default/llama-3-2-1b-base, default/sft-dataset)
  • \n
  • Verify the model entity points at the fileset: fileset=f"default/{MODEL_NAME}"
  • \n
  • Check job status: client.jobs.get_status(name=job.job.name, workspace="default")
  • \n
\n

Job fails with OOM (Out of Memory) error:

\n
    \n
  1. First try: Reduce global_batch_size from 64 to 32 or 16 in batch={...}
  2. \n
  3. Still OOM: Keep micro_batch_size at 1 (already the minimum in this tutorial)
  4. \n
  5. Still OOM: Reduce max_seq_length from 2048 to 1024 or 512 in training={...}
  6. \n
  7. Last resort: Increase num_gpus_per_node and tensor_parallel_size in parallelism={...}
  8. \n
\n

Loss curves not decreasing (underfitting):

\n
    \n
  • Increase training duration: raise epochs from 2 to 3-5 in schedule={...}
  • \n
  • Adjust learning rate: try 1e-4 or 1e-5 instead of the default 5e-5 in optimizer={...}
  • \n
  • Check data quality: Verify formatting, remove duplicates, ensure diversity
  • \n
\n

Training loss decreases but validation loss increases (overfitting):

\n
    \n
  • Reduce epochs from 2 to 1 in schedule={...}
  • \n
  • Lower learning_rate from 5e-5 to 2e-5 or 1e-5 in optimizer={...}
  • \n
  • Increase dataset size and diversity
  • \n
  • Verify train/validation split has no data leakage
  • \n
\n

Model output quality is poor despite good training metrics:

\n
    \n
  • Training metrics optimize for loss, not your actual task—evaluate on real use cases
  • \n
  • Review data quality, format, and diversity—metrics can be misleading with poor data
  • \n
  • Try a different base model size or architecture
  • \n
  • Adjust learning_rate and global_batch_size
  • \n
  • Compare to baseline: Test base model to ensure fine-tuning improved performance
  • \n
\n

Deployment fails:

\n
    \n
  • Verify output model exists: client.models.retrieve(name=OUTPUT_NAME, workspace="default")
  • \n
  • Check deployment logs: client.inference.deployments.get_logs(name=deployment.name, workspace="default")
  • \n
  • Ensure sufficient GPU resources for executor_config={"gpu": 1, ...}
  • \n
  • Verify the deployment config matches this tutorial: engine="vllm" with vllm/vllm-openai:v0.22.1
  • \n
\n

Next Steps

\n\n" } ] }; diff --git a/docs/fern/package.json b/docs/fern/package.json index af860379c7..c42ea3a2a6 100644 --- a/docs/fern/package.json +++ b/docs/fern/package.json @@ -4,9 +4,10 @@ "prepare:openapi": "node scripts/filter-public-openapi.mjs", "prepare:helm": "node scripts/sync-helm-docs.mjs", "prepare": "npm run prepare:openapi && npm run prepare:helm", - "check": "npm run prepare && npx -y fern-api@latest check && npm run validate-mdx && npm run check:gated-links", + "check": "npm run prepare && npx -y fern-api@latest check && npm run validate-mdx && npm run validate-notebook-viewer && npm run check:gated-links", "check:fern": "npm run prepare && npx -y fern-api@latest check", "validate-mdx": "node scripts/validate-mdx.mjs", + "validate-notebook-viewer": "node scripts/validate-notebook-viewer.mjs", "check:gated-links": "node scripts/delink-gated.mjs", "fix:gated-links": "node scripts/delink-gated.mjs --fix", "broken-links": "npm run prepare && npx -y fern-api@latest docs broken-links", diff --git a/docs/fern/scripts/README.md b/docs/fern/scripts/README.md index cbd3de6053..0e599250b1 100644 --- a/docs/fern/scripts/README.md +++ b/docs/fern/scripts/README.md @@ -21,6 +21,9 @@ uv run python docs/fern/scripts/ipynb-to-fern-json.py \ Writes both `.json` (canonical data) and `.ts` (default-export wrapper that MDX imports). Re-run whenever the source `.ipynb` changes. +`npm run validate-notebook-viewer` (part of `npm run check`) fails if a +NotebookViewer registry entry is missing its generated `.ts` / `.json` pair. + ### MDX usage After writing the `.ts` module, register it in `fern/components/NotebookViewer.tsx` diff --git a/docs/fern/scripts/ipynb-to-mdx.py b/docs/fern/scripts/ipynb-to-mdx.py index f8841e6209..6e0e863e4d 100644 --- a/docs/fern/scripts/ipynb-to-mdx.py +++ b/docs/fern/scripts/ipynb-to-mdx.py @@ -29,6 +29,7 @@ r'Download this tutorial as a Jupyter notebook\s*', re.IGNORECASE, ) +FIRST_H1_RE = re.compile(r"\A# [^\n]+\n+") _LINK_REWRITES: list[tuple[re.Pattern[str], str]] = [ ( @@ -51,6 +52,10 @@ re.compile(r"\]\(\.\./\.\./evaluator/index(?:\.md)?\)"), "](/documentation/evaluate-models)", ), + ( + re.compile(r"\]\(\.\./\.\./evaluator/metrics/rag\.md?\)"), + "](/documentation/evaluate-models/metrics/rag-metrics)", + ), ( re.compile(r"\]\(\./distillation-customization-job(?:\.ipynb)?\)"), "](/documentation/customizer-reference/tutorials/distillation-customization-job)", @@ -108,6 +113,7 @@ def colab_link_for(ipynb_path: Path) -> str: def convert_notebook_to_mdx(ipynb_path: Path, *, title: str) -> str: body = NotebookConverter().convert(ipynb_path) body = DOWNLOAD_LINK_RE.sub("", body).lstrip("\n") + body = FIRST_H1_RE.sub("", body, count=1) body = rewrite_links(body) return ( diff --git a/docs/fern/scripts/validate-notebook-viewer.mjs b/docs/fern/scripts/validate-notebook-viewer.mjs new file mode 100644 index 0000000000..82d65fc8ef --- /dev/null +++ b/docs/fern/scripts/validate-notebook-viewer.mjs @@ -0,0 +1,168 @@ +#!/usr/bin/env node +/** + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Ensure every NotebookViewer import is registered, every registration has + * generated notebook data on disk, and that the generated JSON and TypeScript + * artifacts still match their source notebooks. + * + * NotebookViewer.tsx imports `./notebooks/` modules produced by + * `ipynb-to-fern-json.py` and looks them up by name in `const notebooks`. + * An import missing from that registry cannot be resolved by + * ``. A missing `.ts` / `.json` pair breaks + * publication even when the MDX wrapper and registry entry exist. + * + * Run from the fern/ directory: `node scripts/validate-notebook-viewer.mjs`. + */ + +import { access, readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = dirname(fileURLToPath(import.meta.url)); +const VIEWER = join(ROOT, "../components/NotebookViewer.tsx"); +const NOTEBOOKS_DIR = join(ROOT, "../components/notebooks"); +const SOURCE_NOTEBOOKS = { + "distillation-customization-job": join( + ROOT, + "../../customizer/tutorials/distillation-customization-job.ipynb", + ), + "dpo-customization-job": join( + ROOT, + "../../customizer/tutorials/dpo-customization-job.ipynb", + ), + "embedding-customization-job": join( + ROOT, + "../../customizer/tutorials/embedding-customization-job.ipynb", + ), + "lora-customization-job": join( + ROOT, + "../../customizer/tutorials/lora-customization-job.ipynb", + ), + "optimize-throughput": join( + ROOT, + "../../customizer/tutorials/optimize-throughput.ipynb", + ), + "sft-customization-job": join( + ROOT, + "../../customizer/tutorials/sft-customization-job.ipynb", + ), + "tool-calling": join(ROOT, "../../example-applications/tool-calling.ipynb"), +}; + +const IMPORT_RE = + /import\s+\w+\s+from\s+"\.\/notebooks\/([^"]+)";/g; +const REGISTRY_RE = /"([^"]+)":\s*\w+/g; + +const viewerSrc = await readFile(VIEWER, "utf8"); +const imported = [...viewerSrc.matchAll(IMPORT_RE)].map((m) => m[1]); +const registryBlock = viewerSrc.match( + /const notebooks: Record = \{([\s\S]*?)\};/ +)?.[1]; + +if (!registryBlock) { + console.error("validate-notebook-viewer: could not find notebooks registry"); + process.exit(2); +} + +const registered = [...registryBlock.matchAll(REGISTRY_RE)].map((m) => m[1]); +const registeredSet = new Set(registered); +const unregisteredImports = [...new Set(imported)] + .filter((name) => !registeredSet.has(name)) + .sort(); + +if (unregisteredImports.length > 0) { + for (const name of unregisteredImports) { + console.error( + `imported ./notebooks/${name} but missing from NotebookViewer notebooks registry`, + ); + } + console.error( + `\nvalidate-notebook-viewer: ${unregisteredImports.length} imported notebook(s) are not registered. ` + + `Add each name to the const notebooks map in NotebookViewer.tsx.`, + ); + process.exit(1); +} + +// Only registered notebooks are resolvable via . +const names = [...registeredSet].sort(); + +let failed = 0; +for (const name of names) { + for (const ext of ["ts", "json"]) { + const path = join(NOTEBOOKS_DIR, `${name}.${ext}`); + try { + await access(path); + } catch { + failed += 1; + console.error(`missing ${path}`); + } + } + + const sourcePath = SOURCE_NOTEBOOKS[name]; + if (!sourcePath) { + failed += 1; + console.error(`missing source-notebook mapping for ${name}`); + continue; + } + + try { + const notebook = JSON.parse(await readFile(sourcePath, "utf8")); + const jsonPath = join(NOTEBOOKS_DIR, `${name}.json`); + const tsPath = join(NOTEBOOKS_DIR, `${name}.ts`); + const artifact = JSON.parse(await readFile(jsonPath, "utf8")); + const sourceCells = notebook.cells.map((cell) => + (Array.isArray(cell.source) ? cell.source.join("") : cell.source ?? "").trimEnd(), + ); + const artifactCells = artifact.cells.map((cell) => + (cell.source ?? "").trimEnd(), + ); + + const mismatch = + sourceCells.length !== artifactCells.length || + sourceCells.some((source, index) => source !== artifactCells[index]); + if (mismatch) { + failed += 1; + console.error( + `stale ${jsonPath}; regenerate it from ${sourcePath}`, + ); + } + + const tsSource = await readFile(tsPath, "utf8"); + const tsMatch = tsSource.match(/export\s+default\s+\{\s*cells:\s*(\[[\s\S]*\])\s*\};\s*$/); + if (!tsMatch) { + failed += 1; + console.error(`could not parse default export in ${tsPath}`); + } else { + const tsCells = JSON.parse(tsMatch[1]).map((cell) => + (cell.source ?? "").trimEnd(), + ); + const tsMismatch = + artifactCells.length !== tsCells.length || + artifactCells.some((source, index) => source !== tsCells[index]); + if (tsMismatch) { + failed += 1; + console.error( + `stale ${tsPath}; regenerate it from ${sourcePath}`, + ); + } + } + } catch (error) { + failed += 1; + console.error(`could not compare ${name} with ${sourcePath}: ${error.message}`); + } +} + +if (failed > 0) { + console.error( + `\nvalidate-notebook-viewer: ${failed} missing or stale notebook artifact(s). ` + + `Run: uv run python docs/fern/scripts/ipynb-to-fern-json.py ` + + `-o docs/fern/components/notebooks/.json` + ); + process.exit(1); +} + +console.log( + `validate-notebook-viewer: ${names.length} NotebookViewer notebook(s) present` +); diff --git a/docs/requirements.mdx b/docs/requirements.mdx index df3cb5b43f..06cf23c27e 100644 --- a/docs/requirements.mdx +++ b/docs/requirements.mdx @@ -39,8 +39,8 @@ Local provider workflows do not require a local GPU. GPU requirements apply only | Component | Requirement | Notes | |-----------|-------------|-------| | GPU | NVIDIA data center GPU with 40 GB VRAM minimum; 80 GB recommended | A100 80GB, H100 80GB, and B200 180GB meet the recommended profile. | -| CUDA | CUDA 12.8 or later | GPU Python dependencies are built for CUDA 12.8. | -| NVIDIA driver | CUDA 12.8-capable driver, R570 branch or later | See the [CUDA 12.8 release notes](https://docs.nvidia.com/cuda/archive/12.8.0/cuda-toolkit-release-notes/index.html) for exact driver minimums by operating system. | +| CUDA | CUDA 13 or later | GPU workloads require CUDA 13 or later. | +| NVIDIA driver | CUDA 13-capable driver, R580 branch or later | See the [CUDA 13.0 release notes](https://docs.nvidia.com/cuda/archive/13.0.0/cuda-toolkit-release-notes/index.html) for exact driver minimums by operating system. | | Platform | Linux x86_64 | Local NVIDIA GPU workloads are not supported on macOS or Windows. | ## Verify Your Environment diff --git a/docs/support-matrix.mdx b/docs/support-matrix.mdx index f986eb5c21..7ba33aa46c 100644 --- a/docs/support-matrix.mdx +++ b/docs/support-matrix.mdx @@ -31,8 +31,8 @@ self-managed Kubernetes deployments installed with the NeMo Platform Helm chart. | Area | Supported | Notes | |------|-----------|-------| -| CUDA | CUDA 12.8 or later | GPU Python dependencies are built for CUDA 12.8. | -| NVIDIA driver | CUDA 12.8-capable driver, R570 branch or later | See the [CUDA 12.8 release notes](https://docs.nvidia.com/cuda/archive/12.8.0/cuda-toolkit-release-notes/index.html) for exact driver minimums by operating system. | +| CUDA | CUDA 13 or later | GPU workloads require CUDA 13 or later. | +| NVIDIA driver | CUDA 13-capable driver, R580 branch or later | See the [CUDA 13.0 release notes](https://docs.nvidia.com/cuda/archive/13.0.0/cuda-toolkit-release-notes/index.html) for exact driver minimums by operating system. | | GPU memory | 40 GB VRAM minimum; 80 GB VRAM recommended | Smaller GPUs can run provider or client workflows, but local model and GPU-accelerated jobs require larger GPUs. | | Recommended GPUs | NVIDIA A100 80GB, H100 80GB, B200 180GB | Use comparable data center GPUs that meet the CUDA and memory requirements for the model or job. | | GPU platform | Linux x86_64 | macOS and Windows are not supported for local NVIDIA GPU workloads. | @@ -64,4 +64,4 @@ The following are not part of the OSS support matrix: - WSL-based local install or GPU validation. - macOS local GPU workloads. - Python earlier than 3.11 or Python 3.15 and later for the documented OSS local install path. -- CUDA versions earlier than 12.8 for GPU-enabled Python dependencies. +- CUDA versions earlier than 13 for GPU workloads. diff --git a/docs/troubleshooting/customizer.mdx b/docs/troubleshooting/customizer.mdx index b84c7f5dbd..7ea0b28651 100644 --- a/docs/troubleshooting/customizer.mdx +++ b/docs/troubleshooting/customizer.mdx @@ -2,13 +2,17 @@ title: "Troubleshooting NeMo Customizer" description: "" --- + **Job fails during model download:** -- Verify the HuggingFace token secret is configured correctly -- Accept the model's license on the [HuggingFace model page](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) + +- Verify the Hugging Face token secret is configured correctly +- Accept the model's license on the [Hugging Face model page](https://huggingface.co/meta-llama/Llama-3.2-1B-Instruct) - Check job status: `client.jobs.get_status(name=job.name, workspace="default")` **Job fails with disk full or 500 error when retrieving logs:** -- The platform's shared persistent volume is likely full. Customization jobs require significant disk space: ~3× model size for full SFT, ~1.5× for LoRA. If you are also deploying the model from a base checkpoint fileset, plan for ~2.5× model size overall. + +- The platform's shared persistent volume is likely full. Budget against the downloaded base checkpoint size: approximately 3× for Full SFT and 1.5× for LoRA. For example, a 70B BF16 checkpoint is approximately 140 GB, so a Full SFT job can require approximately 420 GB of free disk at peak. +- These peak estimates include the base checkpoint and job artifacts; the final Full SFT output itself is one full checkpoint. If you also retain a deployment copy, include it separately in capacity planning. - Clean up completed job artifacts or increase the PVC size (default: 200Gi at `/var/run/scratch/job`). - DPO/GRPO jobs also consume ephemeral node storage under `/tmp` via Ray workers — check node disk in addition to the PVC. - See [ft-tut-understand-models](/documentation/customizer-reference/tutorials/understanding-models-and-training) for full storage requirement details. @@ -27,17 +31,20 @@ Batch and sequence-length fields differ by backend. Use the fully qualified path 3. Reduce `model.max_seq_length` from 2048 to 1024 or 512 **Training loss not decreasing:** + - Increase `optimizer.learning_rate` (try 2e-4 or 5e-4), same path for Automodel and Unsloth - Increase `schedule.epochs`, same path for Automodel and Unsloth - Verify data quality -- inspect a few training examples manually **Tool calling accuracy is low after fine-tuning:** + - Increase training data size (sample more from the filtered dataset) - Increase `schedule.epochs` to a higher value. If you are running for 1-2 epochs, increase it to 3-4. - Check that the evaluation dataset format matches what the model expects - Verify the base model supports tool calling (Llama 3.2 Instruct does) **Deployment fails:** + - Verify the base model and adapter exist: `client.models.retrieve(name=MODEL_NAME, workspace="default")` -- the LoRA adapter appears in the base model's `adapters` list, not as a separate model entity - Check deployment logs: `client.inference.deployments.get_logs(name=deployment.name, workspace="default")` - Ensure sufficient GPU resources for the model size