Skip to content

Commit 269b712

Browse files
committed
Generic
1 parent cf890fd commit 269b712

3 files changed

Lines changed: 63 additions & 43 deletions

File tree

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

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ Training never runs inside the `nemo` CLI process. After `submit`, the platform'
113113
- Resolve the CLI per **Pre-flight — CLI resolution** before any `nemo …` command; run from the **nemo-platform** git root, not a plugin subfolder.
114114
- Set `NEMO_BASE_URL` (or `NMP_BASE_URL`) only when the user gives a platform URL; default `http://127.0.0.1:8080` (same as `http://localhost:8080`). Track whether the user **overrode** the base URL — see **Platform unreachable** below.
115115
- **Platform unreachable** — if any platform API call fails with a connection error (`Connection error`, timeout, refused):
116-
- **User gave a custom URL** (e.g. `10.0.0.51:8080`) or you exported a non-default `NEMO_BASE_URL` / `NMP_BASE_URL`: stop and tell the user the platform is not reachable at that address. Do **not** offer to start local services.
116+
- **User gave a custom URL** (e.g. `$NMP_BASE_URL`) or you exported a non-default `NMP_BASE_URL` / `NEMO_BASE_URL`: stop and tell the user the platform is not reachable at that address. Do **not** offer to start local services.
117117
- **Default URL only** (no user override): **ask** whether to start the platform locally. If they agree, from the **nemo-platform** git root run in the **background**:
118118

119119
```bash
@@ -139,7 +139,7 @@ Training never runs inside the `nemo` CLI process. After `submit`, the platform'
139139
- **Do not use local `docker info`** to pick automodel vs unsloth. Run `nemo jobs list-execution-profiles -f json` against the user's platform (login first only if auth is enabled — see **Authentication**; see `references/troubleshooting.md`). Default output is a table — **`-f json` is required** for scripting; parse **stdout only** (do not pipe `2>&1` into `json.load`).
140140
- **Do not merge stderr into stdout when parsing JSON** — `submit`, `explain`, and `-f json` commands write **JSON on stdout**; harmless warnings like `Configuration file not found, using defaults` go to **stderr**. Piping with **`2>&1`** before `json.load` raises `JSONDecodeError` even when submit **succeeded** — a common cause of **duplicate jobs** when the agent re-submits after a parse error. Parse stdout only; redirect stderr if needed (`2>/dev/null`). See `references/troubleshooting.md` § **Parsing CLI JSON**.
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`).
142-
- **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**.
142+
- **Missing training image on a remote platform**if the user gave a non-localhost `NMP_BASE_URL` / `NEMO_BASE_URL` 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**.
144144
- **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`.
145145
- **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)**.
@@ -600,8 +600,8 @@ The adapter `<output.name>` is registered on `default/<model-entity>`. Weights a
600600
601601
| Target | Gateway path | OpenAI base URL | Request `"model"` field |
602602
|--------|--------------|-----------------|-------------------------|
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>` |
603+
| **Base** weights | model-entity | `$NMP_BASE_URL/apis/inference-gateway/v2/workspaces/default/model/<model-entity>/-/v1` | `default/<model-entity>` |
604+
| **LoRA adapter** | **provider** | `$NMP_BASE_URL/apis/inference-gateway/v2/workspaces/default/provider/<provider>/-/v1` | `default--<output.name>` |
605605
606606
**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)**.
607607
@@ -619,7 +619,7 @@ Match training context at inference — send **`messages[:-1]`** (all turns exce
619619
#### Example — LoRA adapter via provider
620620
621621
\`\`\`bash
622-
export NEMO_BASE_URL=<platform-url> # omit when using default localhost
622+
export NMP_BASE_URL=<platform-url> # omit when using default localhost; NEMO_BASE_URL also works
623623
nemo inference gateway provider post v1/chat/completions <provider> --workspace default \\
624624
--body '{
625625
"model": "default--<output.name>",
@@ -633,7 +633,7 @@ nemo inference gateway provider post v1/chat/completions <provider> --workspace
633633
#### Example — base model via model-entity (comparison)
634634
635635
\`\`\`bash
636-
export NEMO_BASE_URL=<platform-url>
636+
export NMP_BASE_URL=<platform-url>
637637
nemo inference gateway model post v1/chat/completions <model-entity> --workspace default \\
638638
--body '{
639639
"model": "default/<model-entity>",
@@ -651,18 +651,17 @@ Validation loss from training is **not** accuracy. To compare base vs adapter on
651651
\`\`\`bash
652652
cd /path/to/nemo-platform
653653
uv run python plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/eval_helpers.py \\
654-
--base-url <platform-url> \\
655654
--model-entity <model-entity> \\
656655
--adapter <output.name> \\
657656
--provider <provider> \\
658657
--dataset-fileset <dataset-fileset> \\
659658
--split validation.jsonl
660659
\`\`\`
661660
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.
661+
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. Set `NMP_BASE_URL` (or pass `--base-url`) when the platform is not localhost — the helper reads `$NMP_BASE_URL` / `$NEMO_BASE_URL` by default.
663662
```
664663
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.
664+
Use the user's platform URL in `NMP_BASE_URL` when they overrode it; omit the export line for default `http://127.0.0.1:8080`. Substitute `<provider>`, concrete URLs, 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.
666665
667666
**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`).
668667

plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/eval_helpers.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
LoRA adapters registered on the model entity are hot-reloaded automatically on
77
deployments with ``lora_enabled: true`` — no deployment update before eval.
88
9-
Run from the nemo-platform git root::
9+
Run from the nemo-platform git root (reads ``$NMP_BASE_URL`` / ``$NEMO_BASE_URL`` when
10+
``--base-url`` is omitted)::
1011
12+
export NMP_BASE_URL=http://127.0.0.1:8080
1113
uv run python plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/eval_helpers.py \\
12-
--model-entity qwen3-1.7b --adapter lora-a --adapter lora-b \\
13-
--provider qwen3-1.7b-csqa-lora-deploy --dataset-fileset commonsense_qa --split validation
14+
--model-entity <model-entity> --adapter <adapter-a> --adapter <adapter-b> \\
15+
--provider <provider> --dataset-fileset <dataset-fileset> --split validation.jsonl
1416
1517
Import in agent scripts (add references/ to sys.path or run via uv from repo root).
1618
"""
@@ -19,6 +21,7 @@
1921

2022
import argparse
2123
import json
24+
import os
2225
import re
2326
import urllib.error
2427
import urllib.request
@@ -667,9 +670,22 @@ def build_eval_payload(
667670
return payload
668671

669672

673+
def default_base_url() -> str:
674+
"""Platform URL from env or localhost default."""
675+
return (
676+
os.environ.get("NMP_BASE_URL")
677+
or os.environ.get("NEMO_BASE_URL")
678+
or "http://127.0.0.1:8080"
679+
)
680+
681+
670682
def _parse_args() -> argparse.Namespace:
671683
parser = argparse.ArgumentParser(description="Compare base vs LoRA on CHAT validation JSONL")
672-
parser.add_argument("--base-url", default="http://127.0.0.1:8080")
684+
parser.add_argument(
685+
"--base-url",
686+
default=default_base_url(),
687+
help="Platform URL (default: $NMP_BASE_URL, $NEMO_BASE_URL, or http://127.0.0.1:8080)",
688+
)
673689
parser.add_argument("--workspace", default="default")
674690
parser.add_argument("--model-entity", required=True)
675691
parser.add_argument(

plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/post-training-eval.md

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ Training and evaluation must use the **same CHAT JSONL row shape**:
99
```json
1010
{
1111
"messages": [
12-
{"role": "user", "content": "Question: …\nChoices:\n\nAnswer:"},
13-
{"role": "assistant", "content": "bank"}
12+
{"role": "user", "content": "<user turn or multi-turn prompt>"},
13+
{"role": "assistant", "content": "<label to predict>"}
1414
]
1515
}
1616
```
@@ -23,7 +23,7 @@ Multi-turn rows use the same rule: the **final** `messages[-1]` turn is the assi
2323
| Send **`messages[:-1]`** at inference (exclude only the final assistant label) | Pass full `messages` including the label turn, or use `{"messages": "{{ item.messages }}"}` unfiltered |
2424
| Score against **`messages[-1].content`** (final assistant turn) | Score against a renamed `expected` field unless you also keep `messages` |
2525

26-
Single-turn MCQA (user + assistant) is the degenerate case: `messages[:-1]` is just the user turn.
26+
Single-turn rows (one user prompt + one assistant label) are the degenerate case: `messages[:-1]` is just the user turn.
2727

2828
Automodel and unsloth both train on this shape when `has_chat` is true (see `hf-conversion.md`, `dataset-formats.md`).
2929

@@ -38,7 +38,7 @@ CHAT_REFERENCE_TEMPLATE = "{{ item.messages[-1].content }}"
3838

3939
Import from `references/eval_helpers.py` — do not re-type these in one-off scripts.
4040

41-
## Inference defaults (Qwen3 / thinking models)
41+
## Inference defaults (thinking models, e.g. Qwen3)
4242

4343
| Setting | Recommended | Avoid |
4444
|---------|-------------|-------|
@@ -67,8 +67,10 @@ The model-entity proxy path **always** resolves to the base VirtualModel. Settin
6767

6868
| Target | Gateway route | URL pattern | Request `model` field |
6969
|--------|---------------|-------------|------------------------|
70-
| Base entity | **Model entity** | `{NEMO_BASE_URL}/apis/inference-gateway/v2/workspaces/default/model/<model-entity>/-/v1` | `default/<model-entity>` |
71-
| LoRA adapter | **Provider** | `{NEMO_BASE_URL}/apis/inference-gateway/v2/workspaces/default/provider/<provider>/-/v1` | `default--<adapter-name>` |
70+
| Base entity | **Model entity** | `$NMP_BASE_URL/apis/inference-gateway/v2/workspaces/default/model/<model-entity>/-/v1` | `default/<model-entity>` |
71+
| LoRA adapter | **Provider** | `$NMP_BASE_URL/apis/inference-gateway/v2/workspaces/default/provider/<provider>/-/v1` | `default--<adapter-name>` |
72+
73+
(`NEMO_BASE_URL` is an alias for `NMP_BASE_URL`.)
7274

7375
`eval_helpers.py` auto-discovers a READY provider that serves the base entity (or pass `--provider <name>`). Adapter weights still hot-reload on the deployment — no provider update per adapter.
7476

@@ -83,37 +85,40 @@ Optional sanity checks:
8385

8486
If base and LoRA scores were identical (~99% same outputs), the adapter was almost certainly called through the **model-entity** path. That path always resolves to the base VirtualModel — the `"model": "default--<adapter>"` field in the body is ignored. Fix: route LoRA through the **provider** URL with the same `model` field. `eval_helpers.build_platform_model_target()` and the CLI implement this split automatically.
8587

86-
### MCQA metric interpretation
88+
### Classification / short-answer metric interpretation
8789

88-
For commonsense_qa-style MCQA, treat **`normalized_accuracy`** as the primary metric (`normalize_mcqa_answer` strips `A. foo` / markdown).
90+
For multiple-choice or short-label SFT, treat **`normalized_accuracy`** as the primary metric when labels need normalization (`normalize_mcqa_answer` strips `A. foo`, markdown, etc.).
8991

9092
| Observation | Likely meaning |
9193
|-------------|----------------|
92-
| Base & LoRA both ~59% normalized, within ~1 pp | LoRA hit **model-entity** path (base only) — check `warnings` and gateway logs |
93-
| Base raw exact 0%, normalized ~59% | Normal for base on MCQA (formatted prose answers) |
94-
| LoRA normalized >> base (e.g. 76% vs 59%) | Correct provider routing and real adapter lift |
95-
| Train loss dropped sharply but eval flat | Wrong eval routing or need more epochs — val loss ≠ accuracy |
94+
| Base & LoRA normalized scores match within ~1–2 pp | LoRA likely hit **model-entity** path (base only) — check `warnings` and gateway logs |
95+
| Base raw exact match low, normalized much higher | Normal when the base model emits formatted prose but normalized labels match |
96+
| LoRA normalized clearly above base | Correct provider routing and real adapter lift |
97+
| Train loss dropped sharply but eval flat | Wrong eval routing, mismatched inference settings, or need more epochs — val loss ≠ accuracy |
9698

9799
### Epoch / adapter ablations
98100

99101
Resolve adapter names from completed job specs instead of guessing:
100102

101103
```python
104+
import os
102105
from eval_helpers import list_completed_job_adapters, compare_adapters, build_eval_payload
103106

107+
base_url = os.environ.get("NMP_BASE_URL") or os.environ.get("NEMO_BASE_URL") or "http://127.0.0.1:8080"
108+
104109
jobs = list_completed_job_adapters(
105-
base_url="http://10.0.0.51:8080",
110+
base_url=base_url,
106111
workspace="default",
107-
model_entity="qwen3-1.7b",
108-
dataset_fileset="commonsense_qa",
112+
model_entity="<model-entity>",
113+
dataset_fileset="<dataset-fileset>",
109114
)
110115
# jobs[0].epochs, jobs[0].adapter_name, jobs[0].backend — sorted newest first
111116

112117
summaries = compare_adapters(
113-
base_url="...",
118+
base_url=base_url,
114119
workspace="default",
115-
model_entity="qwen3-1.7b",
116-
adapter_names=[jobs[0].adapter_name, jobs[2].adapter_name],
120+
model_entity="<model-entity>",
121+
adapter_names=[jobs[0].adapter_name, jobs[1].adapter_name],
117122
rows=rows,
118123
)
119124
payload = build_eval_payload(..., summaries=summaries, adapter_names=[...])
@@ -129,7 +134,7 @@ When comparing adapters from **different backends** (automodel vs unsloth) or ba
129134
| HTTP base URL | `…/provider/<provider>/-/v1` | `…/model/<model-entity>/-/v1` |
130135
| `"model"` | `default--<adapter-name>` | `default/<model-entity>` |
131136
| `messages` | `messages[:-1]` from the training row (exclude final assistant label) | Same |
132-
| Qwen3 short SFT | `"chat_template_kwargs": {"enable_thinking": false}` | Same |
137+
| Short-answer SFT (e.g. Qwen3) | `"chat_template_kwargs": {"enable_thinking": false}` | Same |
133138
| `max_tokens` / `temperature` | `64` / `0` typical for short labels | Same |
134139

135140
CLI shortcuts (substitute names from the job):
@@ -158,23 +163,23 @@ Val loss from training is **not** accuracy — always run a generation eval for
158163
From **nemo-platform** git root:
159164

160165
```bash
161-
# Base vs one adapter
166+
export NMP_BASE_URL=http://127.0.0.1:8080 # user platform URL when not localhost
167+
168+
# Base vs one adapter (--base-url optional when NMP_BASE_URL is set)
162169
uv run python plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/eval_helpers.py \
163-
--base-url http://10.0.0.51:8080 \
164-
--model-entity qwen3-1.7b \
165-
--adapter qwen3-1.7b-csqa-unsloth-jun16-e3 \
166-
--provider qwen3-1.7b-csqa-lora-deploy \
167-
--dataset-fileset commonsense_qa \
170+
--model-entity <model-entity> \
171+
--adapter <adapter-name> \
172+
--provider <provider> \
173+
--dataset-fileset <dataset-fileset> \
168174
--split validation.jsonl \
169175
--output /tmp/fine-tune-eval.json
170176

171177
# Base vs multiple adapters (epoch ablation)
172178
uv run python plugins/nemo-customizer/src/nemo_customizer/skills/nemo-customizer/references/eval_helpers.py \
173-
--base-url http://10.0.0.51:8080 \
174-
--model-entity qwen3-1.7b \
175-
--adapter qwen3-1.7b-commonsense-qa-lora-jun12-v2 \
176-
--adapter qwen3-1.7b-csqa-unsloth-jun16-e3 \
177-
--dataset-fileset commonsense_qa \
179+
--model-entity <model-entity> \
180+
--adapter <adapter-a> \
181+
--adapter <adapter-b> \
182+
--dataset-fileset <dataset-fileset> \
178183
--split validation.jsonl \
179184
--output /tmp/fine-tune-eval-multi.json
180185
```

0 commit comments

Comments
 (0)