Authoring do/train Techniques¶
A do/train technique is a directory. Drop one into do/training/ and do/train
discovers it — no change to the script is needed. Add an optional manifest.json and
the technique also gets a typed, tiered, validated hyperparameter UI built on the same
config-builder core that powers do/tune.
See Custom Training for the job-level workflow (datasets,
instances, lifecycle). This page is the reference for the technique plugin surface.
New to it? Start with the walkthrough, which adds the
sft-neftune sample (examples/train-plugins/sft-neftune/) to a project step by step.
Layout¶
do/training/
├── config.yaml # the job (technique, image, instance, dataset, hyperparameters)
├── manifest.schema.json # GENERATED — JSON schema for technique manifests (editor hint)
└── <technique>/
├── train.py # REQUIRED — your training script
├── manifest.json # optional — knob metadata (types, bounds, tiers, help text)
├── defaults.yaml # optional — knob VALUES (the lowest-priority value layer)
└── accelerate_config.yaml # optional — distributed launcher config
train.pyis the only required file. A directory without it is ignored.manifest.jsondescribes knobs; it does not carry values. Put values indefaults.yaml(one value source per technique).prompts.jsonis the pre-Wave-7 way to add interactive questions. It still works for techniques without a manifest. When a manifest exists,prompts.jsonis ignored.
The shipped sft, dpo, and grpo techniques have manifests. custom intentionally
has none: it is the reference for descriptor-less techniques.
The manifest¶
{
"$schema": "../manifest.schema.json",
"schema_version": 1,
"technique": "mytech",
"display_name": "My technique",
"description": "What it trains and on what data.",
"hyperparameters": {
"learning_rate": {
"type": "number",
"required": false,
"tier": "primary",
"display_name": "Learning rate",
"description": "optimizer learning rate"
},
"lora_dropout": {
"type": "number",
"required": false,
"min": 0,
"max": 1,
"tier": "advanced",
"display_name": "LoRA dropout"
},
"scheduler": {
"type": "string",
"required": false,
"enum": ["linear", "cosine"],
"tier": "advanced"
}
}
}
Top-level keys:
| Key | Required | Meaning |
|---|---|---|
$schema |
no | Editor hint. "../manifest.schema.json" gives VS Code validation and autocomplete. Ignored by do/train. |
schema_version |
yes | Always 1. |
technique |
yes | Must equal the directory name. |
display_name, description |
no | Human-readable text. |
hyperparameters |
yes | Knob name → knob entry. Knob names are the keys train.py reads. |
Each knob entry uses the same contract as the do/tune descriptor
(templates/code/tune.d/manifest.schema.json#/$defs/hyperparameter):
| Key | Required | Meaning |
|---|---|---|
type |
yes | string, integer, number, or boolean. Values are coerced and checked against it. |
required |
yes | true makes a missing value an error. |
min, max |
no | Inclusive numeric bounds. Declare them only for a real invariant (a ratio in 0–1, a count ≥ 1). |
step |
no | Numeric step. Enforced for integer knobs; a UI hint for floats. |
enum |
no | Allowed values (non-empty list). Renders as a dropdown. |
default |
no | A fallback value. Prefer defaults.yaml; the shipped manifests declare none. |
tier |
no | primary (default), advanced, or tertiary. See below. |
control |
no | number_input, dropdown, or text_input. Derived when absent (enum → dropdown, numeric → number input, else text). |
display_name, description, hint |
no | Prompt label and help text. |
do/training/manifest.schema.json is generated by npm run codegen from the repo's
config/train-descriptor.schema.json. Don't edit the generated copy.
Tiers¶
| Tier | Prompted with -i |
Prompted with -i --advanced |
Value when not prompted |
|---|---|---|---|
primary |
yes | yes | from the value layers |
advanced |
no | yes | from the value layers |
tertiary |
never | never | from the value layers |
Every knob can always be set with --set, whatever its tier.
Setting values¶
./do/train --technique mytech --set learning_rate=1e-4 --set scheduler=cosine
./do/train --technique sft --learning-rate 1e-4 # legacy alias for --set learning_rate=1e-4
./do/train -i # job questions, then primary knobs
./do/train -i --advanced # ... plus advanced knobs
do/train is non-interactive unless you pass -i/--interactive. --advanced without
-i prints a notice and has no effect. The legacy flags (--learning-rate, --epochs,
--batch-size, --lora-r, --beta) are aliases for --set learning_rate=…,
epochs=…, batch_size=…, lora_r=…, beta=….
Precedence¶
Lowest to highest — the same order do/tune uses:
| # | Layer | Source label in the summary |
|---|---|---|
| 1 | manifest default |
manifest |
| 2 | <technique>/defaults.yaml |
defaults.yaml |
| 3 | config.yaml hyperparameters: |
config.yaml |
| 4 | --set / legacy flag, or an interactive answer |
flag / prompt |
With -i, every knob you did not set by flag is prompted (by tier), and the prompt is
pre-filled with the value from layers 1–3. So your config.yaml values come back as
defaults. Your answers are written back to the hyperparameters: block of
config.yaml. Only that block is rewritten; every other setting stays as you had it.
Comments inside the block are not kept.
Before submitting, do/train prints every hyperparameter the job will get and where
each value came from:
⚙️ Hyperparameters this job will use (sft, manifest.json):
batch_size 4 (defaults.yaml)
epochs 5 (config.yaml)
learning_rate 1e-4 (flag)
Values reach SageMaker as strings, so do/train passes on the string you wrote:
2e-4 stays 2e-4. It validates a typed copy of the value but never re-renders it.
Keys the manifest does not declare¶
- From
defaults.yamlorconfig.yaml: passed totrain.pyunchanged, with a one-line notice. These files are deliberate edits to a script you own. - From
--setor a legacy flag: rejected before anything is submitted. This catches typos thattrain.pywould otherwise ignore without a word:
❌ Hyperparameter validation failed for technique 'sft':
• unknown knob 'learnign_rate' for technique 'sft' — did you mean 'learning_rate'? Declared: batch_size, ... (use --allow-unknown to pass it to train.py anyway)
A legacy flag for a knob the technique doesn't declare is rejected the same way
(for example, --beta on sft).
--allow-unknownpasses undeclared--set/flag keys through totrain.pywith the same notice. Use it when yourtrain.pyreads a key you haven't added to the manifest yet. The better long-term fix is to declare the knob.
Validation errors¶
When a technique has a manifest.json, do/train validates it before resolving any
value or calling AWS. Every problem is reported in one pass, and the run exits with
code 2:
❌ Invalid training manifest: do/training/mytech/manifest.json
• hyperparameters.learning_rate: 'min' (1.0) is greater than 'max' (0.1)
• hyperparameters.scheduler: unknown key 'choices' (did you mean 'enum'?)
→ Fix the manifest, or delete it to fall back to defaults.yaml + flags.
Each line names the knob (or top-level key) and the problem. Checks cover: valid JSON
(with line/column), technique matching the directory, known keys only, type and
required, a non-empty enum, numeric min/max/step with min ≤ max, tier and
control values, and a default that fits its own type, bounds, and enum.
A value error (out of range, wrong type, not in enum, unknown --set key) exits
with code 1, also before any AWS call. No extra Python dependency is needed: the
validator is plain Python.
Descriptor-less techniques¶
A technique without manifest.json (such as custom, or any directory you add) works
exactly as before:
- values merge
defaults.yaml<config.yaml< flags, as strings, with no validation; --set KNOB=VALUEworks too and passes any key through;--advancedand--allow-unknownhave no effect;-iasks the generic epochs/learning-rate/LoRA-rank questions plus anyprompts.jsonquestions.
Add a manifest whenever you want typed prompts and fail-fast validation.
Upgrading an existing project¶
Projects generated before this change keep working with the new do/train:
| What the project has | What happens |
|---|---|
<t>/prompts.json, no manifest.json |
Descriptor-less path, today's behavior (including the prompts.json questions). |
manifest.json and a leftover prompts.json |
The manifest wins; prompts.json is ignored. You can delete it. |
config.yaml with hyperparameters: |
Read as the config.yaml layer. |
What mcc regenerate does to do/training/:
- It re-renders every shipped template file and overwrites it:
config.yaml, andsft/,dpo/,grpo/,custom/files such astrain.pyanddefaults.yaml. Back upconfig.yaml(and any edited shipped recipe) before regenerating, then restore your settings. - It adds the new files:
manifest.schema.jsonandsft/,dpo/,grpo/manifest.json. - It deletes nothing: a leftover
sft/prompts.jsonordpo/prompts.jsonstays, and is ignored because the manifest now exists. - It leaves technique directories you added yourself untouched.
If you don't regenerate, your project keeps its current files and stays on the descriptor-less path for every technique until you add manifests.