Per-Technique Tune Descriptor (tune.d/)¶
The tune descriptor is the single declarative source of truth for a managed
customization technique's reward kind, PEFT variants, dataset contract, and
hyperparameter knobs. It is the tuning analogue of the serve-engine plugin manifest
(serve.d/<engine>/manifest.json, see serve-engine-plugins.md)
and follows the same ADR-008 principle: derive, don't hardcode. Every consumer —
the do/tune config UX (BL128), the register-side dataset producer, and the do/tune
preflight — reads the descriptor so they cannot drift out of agreement.
Introduced in BL129 (.kiro/specs/v2-w3-01-bl129); the decision record is
ADR-011. The settled design rationale +
the live SageMaker JumpStart API probe findings are in
.kiro/v2-w3-tune-descriptor-design.md.
Where it lives¶
templates/code/tune.d/
manifest.schema.json # JSON Schema (draft 2020-12), additionalProperties:false
sft/manifest.json
dpo/manifest.json
rlvr/manifest.json
rlaif/manifest.json
mtrl/manifest.json
The technique set is NOT a hardcoded list — it derives from KNOWN_TECHNIQUES
(src/lib/managed-techniques.js). A conformance test asserts the set of tune.d/*
directories equals KNOWN_TECHNIQUES, so no known technique can ship descriptor-less
and no orphan directory can linger.
Two provenance-tagged layers¶
A descriptor has a STRUCTURAL layer (hand-authored, conformance-tested — the API
does not carry it) and a SYNCED layer (written by scripts/sync-model-families.js
from AWS's recipe override-params — never hand-edited).
{
"technique": "rlaif", // STRUCTURAL — matches the dir name
"reward_source": "judge", // STRUCTURAL — none|rule|preset|judge|agent_env
"variants": {
"lora": {
"peft": "lora", // STRUCTURAL
"artifact_kind": "lora_adapter", // STRUCTURAL — DERIVED from peft (schema-enforced)
"dataset_contract": { // STRUCTURAL
"format": "parquet",
"schema": "verl-5field",
"prompt_shape": "message-list",
"reward_model_style": "model", // verl-5field ONLY (if/then in the schema)
"verified": true
},
"hyperparameters": { /* SYNCED */ },
"_synced": { "smtj": { "from": "s3://…_v2.2.7.json", "version": "2.2.7" } }
}
}
}
STRUCTURAL fields¶
| Field | Meaning |
|---|---|
reward_source |
none (sft) · preset (dpo) · rule (rlvr) · judge (rlaif) · agent_env (mtrl). Drives reward_model_style (rule/preset→rule, judge→model). |
peft |
lora or none per variant. |
artifact_kind |
lora_adapter or full_model, derived from peft and schema-enforced (lora→lora_adapter, none→full_model). Declared here for the later three-way deploy split (BL136/BL137/BL149); BL129 only DECLARES it. |
dataset_contract |
The required training-data shape — format (jsonl|parquet), schema, prompt_shape (string|message-list), verified, and reward_model_style (verl-5field only). |
SYNCED fields¶
hyperparameters is a per-knob map; _synced is a per-backend provenance map
(smtj / hp-eks). Each knob entry always carries type and required, a default
when the source has one, and min/max/step/enum/control/tier/display_name/
description/hint where present. Canonical key names: control (from the AWS
override-param control_type) and tier (from visibility_tier).
Both synced blocks are {} in a pre-sync, hand-authored descriptor. The schema permits
that; the conformance test asserts the post-sync invariant (a synced descriptor carries
at least smtj).
The sync (scripts/sync-model-families.js)¶
One Hub traversal, two outputs: the same RecipeCollection walk that writes
config/tune-catalog.json also populates each descriptor's synced layer. Per recipe,
for its SMTJ override-params object:
- Fetch the override-params JSON (injectable
fetchOverrideParams; default S3 GetObject). The fetch is network-dependent — the unit tests inject an offline fetcher, and a live reachability smoke test is opt-in (MLCC_LIVE_OVERRIDE_PARAMS=1), not in the default gate. - Filter knobs. Two flavors:
- TAGGED (sft/dpo/rlvr/rlaif): keep
category: hyperparameter. - LEANER MTRL (no
category): keep everything NOT in the plumbing DENYLIST (data_path,model_name_or_path,output_path,name). A drift test guards the denylist so a new plumbing key can't silently become a surfaced knob. - Map to the canonical entry shape (
control_type→control,visibility_tier→tier, defaultrequired:false). - Record only the backends that exist — MTRL has
HpEksOverrideParamsS3Uri: None, so its_syncedhassmtjonly. - Additive-merge, preserving the structural layer; idempotent (a re-run with an
unchanged
versionis a no-op). A fetch failure leaves the existing synced layer intact — it never regresses a good descriptor.
The managed-RL dataset contract (Producer + Gate)¶
For the verl-based RL techniques (rlvr/rlaif) the descriptor's dataset_contract
drives both ends of the round trip:
- Producer (
templates/do/lib/python/tune_producer_rl.py, invoked fromtune_stage_hf.py):do/register dataset --technique rlvr|rlaifemits parquet in verl's 5-field row schema —data_source,promptas a chat message list (NOT a flattened string),ability,reward_modelstruct{style, ground_truth},extra_info.reward_model.styleis read from the descriptor (rlvr→rule, rlaif→model), never hardcoded. A flat top-levelground_truthcolumn is lifted into the nested struct (a transform a bare--column-mapcannot do). The produced parquet is asserted conformant at stage time, so a mismatch fails at registration rather than deep in a GPU job. The parquet is written to a channel-puredatasets/<name>/data/subprefix so the_dataset.jsonsidecar is NOT in the training channel. - Gate (
do/tunepreflight +tune_manifest.py): the format half (parquet-required, sidecar-excluded) plus the field-schema half — it readsrequired_dataset_fieldsfrom the descriptor and fails fast naming a missing verl field. A technique whose contract isverified:false(mtrl-rollout) is format-checked only.
MTRL¶
MTRL ships REAL in Wave 3: reward_source: agent_env, synced hyperparameters
(SMTJ-only — HpEksOverrideParamsS3Uri is None), and an honest dataset_contract
marked schema: "mtrl-rollout" / verified: false (its rollout row shape is NOT
fabricated). This makes MTRL configurable + discoverable; RUNNING an MTRL job end-to-end
is deferred (BL139).
Readers¶
| Side | Reader | Used by |
|---|---|---|
| Node (generation-time) | src/lib/tune-manifest-reader.js |
the do/tune config builder (BL128), the generator |
| Python (runtime) | templates/do/lib/python/tune_manifest.py |
the do/tune dataset preflight (Gate), the Producer |
Both read the same tune.d/ files; neither branches on the technique name.
Config UX — technique-aware do/tune (BL128)¶
do/tune's hyperparameter surface is driven by the descriptor's synced hyperparameters
block: the user is offered exactly the selected technique's knobs — with their defaults,
ranges, choice-lists, input widgets, and progressive-disclosure tiers — instead of a fixed
flag set. Shipped in BL128 (.kiro/specs/v2-w3-02-bl128).
Descriptor-source-agnostic core. templates/do/lib/python/tune_config_builder.py has a
core build_config(hyperparameters, supplied, *, interactive, include_advanced, prompter)
that takes a hyperparameters map + supplied values and returns {resolved, errors}. It
knows nothing about tune.d/, technique names, _synced, or reward_source — only the
shared $defs/hyperparameter entry shape. A thin build_from_tune_descriptor adapter (same
module) is the ONLY tune.d-aware entry; it reads tune_manifest.read_hyperparameters and
calls the core. This is deliberate: the user-authored do/train plugin UX (BL154) supplies
its own descriptor source (with its own runtime validation / trust boundary) and reuses the
same core instead of forking it.
What the core does (each a BL128 requirement): derives an effective control/tier for a
knob that omits them (MTRL's leaner schema — enum→dropdown, numeric→number_input, else
text_input; default tier primary); progressive disclosure (prompt primary always,
advanced only on opt-in, never tertiary); validates every value against
type/min/max/step/enum/required; and resolves precedence
descriptor default < config file < interactive answer / flag (user value wins).
Surfaces (all three derive from the descriptor — a drift test asserts they agree):
- Interactive —
do/tuneprompts viaquestionary(modeled ondeploy_prompts.py):dropdown→questionary.selectover theenum, numeric/text→questionary.textpre-filled with the default. Wired in the.tune_helper.py build-configsubcommand. - Flags —
--set KNOB=VALUE(repeatable), keyed by the descriptor's canonical knob name; a knob the technique does not declare is rejected naming the valid set. The five legacy flags (--epochs/--learning-rate/--max-seq-length/--lora-rank/--batch-size) remain as validated aliases onto the same map.--advancedopts into the advanced tier;--non-interactive(or no TTY) applies defaults with the same validation. - Submission — the resolved
{knob: value}map flows to.tune_helper.py submitas a single--hyperparameters <json>blob thattune_submit.pymerges into the trainer's hyperparameters (the canonical knob names match the SDK option names by construction, so no translation table — the old hand-maintained per-flag map was removed).
All validation happens in sub-second preflight (unknown knob, out-of-range value, required knob with no value) — before the SageMaker job is submitted.
Validation¶
scripts/validate-tune-manifests.js (mirrors validate-serve-manifests.js) discovers
every tune.d/<technique>/manifest.json, validates each against the schema, and
cross-checks the technique field against the directory name. It runs as:
npm run validate:tune-manifests- a blocking CI step (
.github/workflows/ci.yml) - a pre-commit guard (husky) when a
tune.d/file is staged
Adding / changing a technique¶
- Add the technique to
KNOWN_TECHNIQUES(src/lib/managed-techniques.js). - Create
templates/code/tune.d/<technique>/manifest.jsonwith the STRUCTURAL layer (leavehyperparameters/_syncedas{}). - Run
npm run validate:tune-manifestsand the conformance test (test/unit/tune-manifest-conformance.test.js). - Run
sync-model-familiesto populate the synced layer (or let the next scheduled sync do it).
Never hand-edit the hyperparameters / _synced blocks — they are sync-owned.