Skip to content

Commit cf890fd

Browse files
committed
feat(customizer): expand skill to help evaluate lift on adapters trained
Signed-off-by: Sam Oluwalana <soluwalana@nvidia.com>
1 parent 40f0d0d commit cf890fd

5 files changed

Lines changed: 1254 additions & 6 deletions

File tree

plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/SKILL.md

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ Training never runs inside the `nemo` CLI process. After `submit`, the platform'
141141
- For submit/image/plugin errors (both backends), read `references/troubleshooting.md`. Unsloth needs the `nmp-unsloth-training` container image on the **platform host's** Docker daemon (see `docker/unsloth/README.md`).
142142
- **Missing training image on a remote platform**if the user gave a non-localhost `NEMO_BASE_URL` / `NMP_BASE_URL` (e.g. `10.0.0.51:8080`) and the job errors with `Failed to pull image`, `manifest unknown`, or missing `nmp-unsloth-training` / automodel training image: **do not** run `docker build`, `docker pull`, or `docker buildx bake` on the agent machine. Report with **Report to user** (use **Output adapter fileset (planned):** on error), then append on-target build steps from `references/troubleshooting.md` § **Missing training images**.
143143
- **Gated HuggingFace models** (Llama, Gemma, …) — confirm `hf-token` + fileset `token_secret` before submit; download fails with `Failed to access upstream storage` / 502 when missing. See **HuggingFace token (gated models)** and `references/troubleshooting.md` § **Gated HuggingFace models**.
144+
- **Post-training eval format** — use the same CHAT `messages` JSONL as training. **Do not** flatten rows to `prompt`/`expected` for the evaluator. Send `messages[:-1]` at inference (exclude final assistant label); score against `messages[-1].content`. See `references/post-training-eval.md` and `references/eval_helpers.py`.
145+
- **LoRA adapters load automatically for eval** — when a job completes, the adapter is registered on the model entity and hot-reloaded on any **READY** deployment with `lora_enabled: true`. **Do not** update deployments or providers before eval. **Do** route LoRA eval through the **provider** gateway (`/provider/<name>/-/v1` with `model: default--<adapter>`); the model-entity path (`/model/<entity>/-/v1`) always hits the base model. See `references/post-training-eval.md` § **Request routing (base vs LoRA)**.
144146

145147
## Workflow
146148

@@ -162,12 +164,14 @@ Common steps then **branch by plugin pick**:
162164
- [ ] nemo customization automodel submit /tmp/job.json --workspace default
163165
- [ ] Poll until top-level terminal (`poll_customization_job.sh`; default 15s interval, or 30–60s manual polls)
164166
- [ ] Report using output template below
167+
- [ ] Optional: compare base vs adapter on validation — `references/eval_helpers.py …` (CHAT format; adapters hot-reload automatically; see `references/post-training-eval.md`)
165168
166169
# unsloth branch (submit → Docker GPU job)
167170
- [ ] Write /tmp/job.json using the UnslothJobInput shape (see Fast path — unsloth)
168171
- [ ] nemo customization unsloth submit /tmp/job.json --workspace default [--profile <gpu-profile>]
169172
- [ ] Poll until top-level terminal (`poll_customization_job.sh unsloth-<job-id>`; default 15s interval)
170173
- [ ] Report using output template below
174+
- [ ] Optional: compare base vs adapter on validation — `references/eval_helpers.py …` (CHAT format; adapters hot-reload automatically; see `references/post-training-eval.md`)
171175
```
172176
173177
## Fast path — automodel
@@ -513,7 +517,7 @@ After polling reaches a **terminal** status (`completed`, `error`, or `cancelled
513517
514518
| Status | Notes |
515519
|--------|-------|
516-
| `completed` | Brief success summary (e.g. adapter registered on model entity). When `metrics.train_loss` has ≥2 entries, add a loss-drop sentence: *Loss dropped from \<first value, 1 dp\> at step 1 to \<last value, 3 dp\> at step \<N\>; validation loss was \<val or n/a\>.* |
520+
| `completed` | Brief success summary (e.g. adapter registered on model entity). When `metrics.train_loss` has ≥2 entries, add a loss-drop sentence: *Loss dropped from \<first value, 1 dp\> at step 1 to \<last value, 3 dp\> at step \<N\>; validation loss was \<val or n/a\>.* Always append **Using the adapter** with discovered provider name and concrete gateway URLs (see below). |
517521
| `error` | Quote `error_details.message` or the failing step; note setup that succeeded before the failure (auth, dataset upload, submit). |
518522
| `cancelled` | Cancellation reason if available. |
519523
@@ -580,21 +584,85 @@ After polling reaches a **terminal** status (`completed`, `error`, or `cancelled
580584
| Output save method | lora |
581585
```
582586
583-
**Using the adapter (`completed` only)** — after **Training configuration**, run `nemo models get <model-entity> --workspace default` (parse stdout only) to confirm the adapter is listed under `adapters`. Append this section:
587+
**Using the adapter (`completed` only)** — after **Training configuration**, run these discovery commands (parse stdout only; do not pipe `2>&1` into JSON parsers):
588+
589+
1. `nemo models get <model-entity> --workspace default` — confirm `<output.name>` appears under `adapters` with `enabled: true`.
590+
2. `nemo inference providers list --workspace default -f json` — pick a **READY** provider whose `served_models` includes `default/<model-entity>` (base or LoRA composite). Record its `name` as `<provider>` (often matches the deployment name).
591+
592+
On a deployment with `lora_enabled: true`, the adapter is **hot-reloaded automatically** — no deployment update or provider reconfiguration is required before inference or post-training eval. Append this section with **concrete URLs and provider name** from discovery:
584593
585594
```markdown
586595
### Using the adapter
587596
588-
The adapter `<output.name>` is attached to `default/<model-entity>`. List adapters with:
597+
The adapter `<output.name>` is registered on `default/<model-entity>`. Weights are hot-reloaded on LoRA-enabled deployments — no deployment or provider update is required after training.
598+
599+
#### Request routing (base vs LoRA)
600+
601+
| Target | Gateway path | OpenAI base URL | Request `"model"` field |
602+
|--------|--------------|-----------------|-------------------------|
603+
| **Base** weights | model-entity | `<NEMO_BASE_URL>/apis/inference-gateway/v2/workspaces/default/model/<model-entity>/-/v1` | `default/<model-entity>` |
604+
| **LoRA adapter** | **provider** | `<NEMO_BASE_URL>/apis/inference-gateway/v2/workspaces/default/provider/<provider>/-/v1` | `default--<output.name>` |
605+
606+
**Common mistake:** posting to the model-entity URL with `"model": "default--<output.name>"` still runs the **base** model. Base-vs-adapter eval will look identical until LoRA requests use the **provider** URL above. See `references/post-training-eval.md` § **Request routing (base vs LoRA)**.
607+
608+
#### Chat inference (CHAT-trained models)
609+
610+
Match training context at inference — send **`messages[:-1]`** (all turns except the final assistant label). Single-turn rows are just the user message; multi-turn rows keep prior user/assistant history.
611+
612+
| Setting | Value | Why |
613+
|---------|-------|-----|
614+
| `messages` | All turns except the final assistant label from the JSONL row | Same decode path as SFT |
615+
| `max_tokens` | `64` for short assistant labels | Training targets are brief (e.g. MCQA choice text) |
616+
| `temperature` | `0` | Reproducible eval / regression checks |
617+
| `chat_template_kwargs.enable_thinking` | `false` for Qwen3 short-answer SFT | Thinking mode needs extra tokens and changes output shape vs training |
618+
619+
#### Example — LoRA adapter via provider
620+
621+
\`\`\`bash
622+
export NEMO_BASE_URL=<platform-url> # omit when using default localhost
623+
nemo inference gateway provider post v1/chat/completions <provider> --workspace default \\
624+
--body '{
625+
"model": "default--<output.name>",
626+
"messages": [<all turns except final assistant label from the eval row>],
627+
"max_tokens": 64,
628+
"temperature": 0,
629+
"chat_template_kwargs": {"enable_thinking": false}
630+
}'
631+
\`\`\`
632+
633+
#### Example — base model via model-entity (comparison)
634+
635+
\`\`\`bash
636+
export NEMO_BASE_URL=<platform-url>
637+
nemo inference gateway model post v1/chat/completions <model-entity> --workspace default \\
638+
--body '{
639+
"model": "default/<model-entity>",
640+
"messages": [<same prompt turns as LoRA example — exclude final assistant label>],
641+
"max_tokens": 64,
642+
"temperature": 0,
643+
"chat_template_kwargs": {"enable_thinking": false}
644+
}'
645+
\`\`\`
646+
647+
#### Post-training eval (optional)
648+
649+
Validation loss from training is **not** accuracy. To compare base vs adapter on the validation split with correct routing:
589650
590651
\`\`\`bash
591-
export NEMO_BASE_URL=<platform-url> # omit line when using default localhost
592652
cd /path/to/nemo-platform
593-
nemo models get <model-entity> --workspace default
653+
uv run python plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/eval_helpers.py \\
654+
--base-url <platform-url> \\
655+
--model-entity <model-entity> \\
656+
--adapter <output.name> \\
657+
--provider <provider> \\
658+
--dataset-fileset <dataset-fileset> \\
659+
--split validation.jsonl
594660
\`\`\`
661+
662+
Uses CHAT `messages` rows unchanged from the training fileset (`messages[:-1]` at inference). Repeat `--adapter` for multi-adapter compare. `--provider` is optional when a READY provider is auto-discovered.
595663
```
596664
597-
Use the user's platform URL in `NEMO_BASE_URL` when they overrode it; omit the export line for default `http://127.0.0.1:8080`. The JSON `adapters` array shows `name`, `fileset`, `finetuning_type`, and `lora_config` for each registered adapter.
665+
Use the user's platform URL in `NEMO_BASE_URL` when they overrode it; omit the export line for default `http://127.0.0.1:8080`. Substitute `<provider>`, `<NEMO_BASE_URL>`, and entity/adapter names with values from discovery — do not leave generic placeholders in the user-facing report. Do **not** tell the user to update the deployment or add the adapter to a provider before calling it — registration on the model entity is sufficient.
598666
599667
**Save report to `/tmp`** — unless the user opts out, write the full Markdown report (header, **Training configuration**, **Using the adapter** when `completed`, and **Resources created** when a slug or new filesets were used) to `/tmp/fine-tune-result-<slug-or-job-suffix>.md`. Use the random slug from the run when one was assigned; otherwise use the job id suffix (e.g. `a925b07ff678`).
600668
@@ -626,5 +694,6 @@ For other terminal errors, keep the same header template; put remediation detail
626694
| W&B / MLflow field reference | `references/hyperparameters.md` § **Integrations (automodel + unsloth)** |
627695
| W&B secret + MLflow local server + jobs-launcher | `references/integrations-setup.md` |
628696
| Gated HF model auth (`hf-token`, fileset `token_secret`) | `references/troubleshooting.md` § **Gated HuggingFace models** |
697+
| Post-training eval (base vs LoRA, CHAT format parity) | `references/post-training-eval.md`, `references/eval_helpers.py` |
629698
630699
Related: `plugins/nemo-automodel/README.md`, `plugins/nemo-unsloth/README.md`, `plugins/nemo-customizer/docs/CUSTOMIZATION.md`, skills **`nemo-files`**, **`nemo-status`**, **`nemo-secrets`**.

plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/dataset-formats.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,15 @@ Optional fields on the unsloth `dataset` block:
5454
- The automodel SFT format `{"prompt": "...", "completion": "..."}` is **not** directly consumable by unsloth — unsloth has no built-in `prompt`/`completion` concatenation. Convert to either messages or pre-rendered text before upload.
5555

5656
EMBEDDING and CUSTOM (automodel-only schemas) are not supported by unsloth today.
57+
58+
## Post-training evaluation
59+
60+
Eval rows must use the **same CHAT `messages` shape** as training. Do not flatten to `prompt`/`expected` for the evaluator.
61+
62+
| Training JSONL | Eval dataset | Eval `prompt_template` | Metric reference |
63+
|----------------|--------------|------------------------|------------------|
64+
| `messages` (single- or multi-turn) | Same fileset split (`validation.jsonl`) | `messages[:-1]` — exclude final assistant label — see `post-training-eval.md` | `{{ item.messages[-1].content }}` |
65+
66+
LoRA inference and eval use the **provider** gateway (`/provider/<name>/-/v1`, `model: default--<adapter>`). Base uses the **model-entity** path. See `post-training-eval.md` § **Request routing** and the **Using the adapter** section in `SKILL.md`.
67+
68+
Shared helpers and compare CLI: `references/eval_helpers.py`. Full workflow: `references/post-training-eval.md`.

0 commit comments

Comments
 (0)