ForgeLM uses YAML files for all configuration — declarative, version-controllable, and CI/CD-ready.
See config_template.yaml for a complete annotated example.
| Field | Type | Default | Description |
|---|---|---|---|
name_or_path |
string | required | HuggingFace model ID or local path |
max_length |
int | 2048 |
Maximum context length |
load_in_4bit |
bool | true |
Enable QLoRA 4-bit NF4 quantization |
backend |
string | "transformers" |
"transformers" or "unsloth" (faster on supported architectures, Linux only) |
trust_remote_code |
bool | false |
Allow custom code from model repos. Security risk — only enable for models that require it |
offline |
bool | false |
Sets the HF offline variables, so Hub, transformers and datasets make no network calls. Models/datasets must be local. Does not stop webhooks, judge or synthetic-data API clients, or W&B / MLflow |
revision |
string | null |
Pin the base model + tokenizer to an HF Hub commit SHA (40-hex) or a branch/tag. Honoured today. See Hub revision pinning |
bnb_4bit_use_double_quant |
bool | true |
Double quantization for extra VRAM savings |
bnb_4bit_quant_type |
string | "nf4" |
Quantization type ("nf4" or "fp4") |
bnb_4bit_compute_dtype |
string | "auto" |
Compute dtype: "auto", "bfloat16", "float16", "float32" (each of the last three also accepts the short alias "bf16", "fp16", "fp32") |
| Field | Type | Default | Description |
|---|---|---|---|
quantize_experts |
bool | false |
Quantize inactive expert weights to int8 for VRAM savings |
experts_to_train |
string | "all" |
"all" or comma-separated expert indices (e.g., "0,1,2") |
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable vision-language model fine-tuning |
image_column |
string | "image" |
Column name for image paths/URLs in dataset |
text_column |
string | "text" |
Column name for text/captions |
| Field | Type | Default | Description |
|---|---|---|---|
r |
int | 8 |
LoRA rank. Higher = more parameters |
alpha |
int | 16 |
LoRA scaling factor |
dropout |
float | 0.1 |
Dropout probability |
bias |
string | "none" |
"none", "all", or "lora_only" |
method |
string | "lora" |
PEFT method: "lora", "dora", "pissa", "rslora" |
use_dora |
bool | false |
Deprecated boolean shortcut for method: "dora"; will be removed in v1.0.0. Setting true forwards to method: "dora" with a DeprecationWarning. Use method instead. |
use_rslora |
bool | false |
Deprecated boolean shortcut for method: "rslora" (recommended for r>64); will be removed in v1.0.0. Setting true forwards to method: "rslora" with a DeprecationWarning. Use method instead. |
target_modules |
list | ["q_proj", "v_proj"] |
Model modules to apply LoRA |
task_type |
string | "CAUSAL_LM" |
Task type for PEFT |
use_doraanduse_rsloraare mutually exclusive, and each conflicts with an explicitly-setmethodthat names a different PEFT method (e.g.use_dora: truewithmethod: "rslora") — either combination raisesConfigError(exit 1) at config-load time. Setmethoddirectly instead of the deprecated boolean flags.Removal will land in v1.0.0, not sooner, because removing a YAML field is a MAJOR change under the versioning policy — see
docs/standards/release.md.
| Field | Type | Default | Description |
|---|---|---|---|
output_dir |
string | "./checkpoints" |
Checkpoint save directory |
final_model_dir |
string | "final_model" |
Isolated relative model subdirectory; see the path policy below |
merge_adapters |
bool | false |
Merge adapters into base model before saving |
trainer_type |
string | "sft" |
"sft", "dpo", "simpo", "kto", "orpo", "grpo" |
max_steps |
int | -1 |
Hard step cap. -1 = use num_train_epochs; a positive value overrides epochs. |
num_train_epochs |
int | 3 |
Number of training epochs (only consulted when max_steps == -1). |
per_device_train_batch_size |
int | 4 |
Batch size per GPU |
gradient_accumulation_steps |
int | 2 |
Steps to accumulate before backward pass |
learning_rate |
float | 2e-5 |
Learning rate (lower for alignment: 5e-6) |
warmup_ratio |
float | 0.1 |
Warmup proportion |
weight_decay |
float | 0.01 |
AdamW weight decay |
eval_steps |
int | 200 |
Evaluate every N steps |
save_steps |
int | 200 |
Save checkpoint every N steps. When a validation split exists it must be an exact multiple of eval_steps, otherwise the run exits 1 at start-up (it is checked once the split is known, so --dry-run cannot see it); GRPO is exempt |
save_total_limit |
int | 3 |
Max checkpoints to keep |
early_stopping_patience |
int | 3 |
Stop after N evals without validation-loss improvement (active only when a validation split exists). |
packing |
bool | false |
Sequence packing (SFT only) |
report_to |
string | "tensorboard" |
"tensorboard", "wandb", "mlflow", "none" |
run_name |
string | null |
W&B/MLflow run name (auto-generated if null) |
final_model_dir must name a separate relative subdirectory of output_dir; final_model and
models/final are valid. The value is preserved as written. Empty values, the output root itself
(.), parent traversal (..), absolute or drive-qualified paths, backslash separators, trailing / or /., existing
files, and symlink components below the output root are rejected. Use / for nested paths on every platform.
The first component cannot be compliance, benchmark, safety, judge, runs, audit_log.jsonl,
audit_log.jsonl.manifest.json, data_audit_report.json, or checkpoint-* (case-insensitive).
Resolved overlap with these run records is also rejected, including when a reserved path aliases
into a proposed model directory. This keeps mutable records out of the integrity manifest and
prevents auto-revert from deleting them. Config loading and --dry-run reject invalid layouts with
exit 1; the trainer rechecks before initialization, saving, finalization, and deletion.
When benchmarks are enabled, a custom evaluation.benchmark.output_dir must also remain separate
from the final or staged model directory: equal, ancestor, and descendant resolved paths are rejected.
Relative benchmark paths resolve from the process working directory, matching the benchmark writer.
Automatically halves per_device_train_batch_size and doubles gradient_accumulation_steps
on CUDA out-of-memory errors, preserving the effective batch size. Retries until the minimum
batch size is reached.
| Field | Type | Default | Description |
|---|---|---|---|
oom_recovery |
bool | false |
Retry training with smaller batch size on CUDA OOM |
oom_recovery_min_batch_size |
int | 1 |
Stop retrying when batch size reaches this value |
Example:
training:
per_device_train_batch_size: 8
gradient_accumulation_steps: 2
oom_recovery: true
oom_recovery_min_batch_size: 1 # try down to batch_size=1 before failingEffective batch size (per_device_train_batch_size × gradient_accumulation_steps) is preserved
across retries. Each retry attempt is logged to the audit trail.
| Field | Type | Default | Description |
|---|---|---|---|
galore_enabled |
bool | false |
Enable GaLore gradient low-rank projection |
galore_optim |
string | "galore_adamw" |
GaLore optimizer variant. One of: "galore_adamw", "galore_adamw_8bit", "galore_adafactor", "galore_adamw_layerwise", "galore_adamw_8bit_layerwise", "galore_adafactor_layerwise". _8bit halves optimizer-state VRAM; _layerwise cuts peak VRAM by recomputing per-layer. |
galore_rank |
int | 128 |
Rank for gradient projection |
galore_update_proj_gap |
int | 200 |
Steps between projection updates |
galore_scale |
float | 0.25 |
GaLore scaling factor |
galore_proj_type |
string | "std" |
Projection type: "std", "reverse_std", "right", "left", "full" |
galore_target_modules |
Optional[List[str]] |
null |
Module-name regex patterns GaLore is applied to. null falls back to [r".*.attn.*", r".*.mlp.*"] (attention + MLP layers). |
| Field | Type | Default | Description |
|---|---|---|---|
rope_scaling |
Optional[Dict[str, Any]] |
null |
RoPE scaling method dict ({"type": "linear", "factor": 2.0} etc.). Supported types: "linear", "dynamic", "yarn", "longrope". |
neftune_noise_alpha |
float | null |
NEFTune noise injection alpha (e.g., 5.0) |
sliding_window_attention |
int | null |
Sliding window attention size in tokens |
sample_packing |
bool | false |
Deprecated alias for packing (TRL exposes a single packing knob). Setting true forwards to packing: true with a DeprecationWarning; will be removed in v1.0.0. Use packing instead. |
Removal will land in v1.0.0, not sooner, because removing a YAML field is a MAJOR change under the versioning policy — see
docs/standards/release.md.
| Field | Type | Default | Description |
|---|---|---|---|
gpu_cost_per_hour |
float | null |
Custom GPU cost rate (USD/hour). Auto-detected from GPU model if null |
| Field | Type | Default | Used By |
|---|---|---|---|
dpo_beta |
float | 0.1 |
DPO temperature |
simpo_gamma |
float | 0.5 |
SimPO margin term |
simpo_beta |
float | 2.0 |
SimPO scaling |
kto_beta |
float | 0.1 |
KTO loss parameter |
orpo_beta |
float | 0.1 |
ORPO odds ratio weight |
grpo_num_generations |
int | 4 |
GRPO: responses per prompt |
grpo_max_completion_length |
int | 512 |
GRPO: max tokens per completion (legacy alias grpo_max_new_tokens accepted) |
grpo_reward_model |
string | null |
GRPO: reward model path (HF or local) |
grpo_reward_model_revision |
string | null |
Pin the GRPO reward model to an HF Hub commit SHA or ref. Rejected without grpo_reward_model. Honoured today — pins the reward tokenizer and the sequence-classification model at the same commit. See Hub revision pinning |
| Field | Type | Default | Description |
|---|---|---|---|
dataset_name_or_path |
string | required | HF dataset ID or local JSONL path |
extra_datasets |
list | null |
Additional datasets to mix in |
mix_ratio |
list | null |
Weight per dataset (e.g., [0.7, 0.3]) |
shuffle |
bool | true |
Shuffle training data |
clean_text |
bool | true |
Strip extra whitespace |
add_eos |
bool | true |
Add EOS token to sequences |
| Field | Type | Default | Description |
|---|---|---|---|
collection_method |
string | "" |
How data was collected |
annotation_process |
string | "" |
Annotation methodology |
known_biases |
string | "" |
Known dataset biases |
personal_data_included |
bool | false |
Contains personal data |
dpia_completed |
bool | false |
Data Protection Impact Assessment done |
| Field | Type | Default | Description |
|---|---|---|---|
auto_revert |
bool | false |
Delete model if evaluation fails |
max_acceptable_loss |
float | null |
Hard ceiling for eval_loss. Must be a finite, non-negative number — .nan / .inf are rejected at load time, because loss > nan is always false and a non-finite ceiling would make the gate pass every model it was asked to reject |
baseline_loss |
float | null |
Computed automatically if null. Same finite, non-negative constraint |
require_human_approval |
bool | false |
Pause for human review (exit code 4) |
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable lm-eval-harness benchmarks |
tasks |
list | [] |
Task names (e.g., ["arc_easy", "hellaswag"]) |
num_fewshot |
int | null |
Few-shot examples (task default) |
batch_size |
string | "auto" |
Evaluation batch size |
limit |
int | null |
Samples per task (for quick checks) |
output_dir |
string | null |
Where to write the benchmark results JSON. null = the training output_dir. |
min_score |
float | null |
Minimum average accuracy |
enabled: truerequires at least one entry intasks— an enabled benchmark gate with no tasks is rejected at config-load time.
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable safety classifier evaluation |
classifier |
string | "meta-llama/Llama-Guard-3-8B" |
Safety classifier model. The shipped default works out of the box: under classifier_mode: auto it is scored via generation-based Llama-Guard scoring |
classifier_mode |
string | "auto" |
How the classifier is scored: auto (generation for a known generative Llama-Guard checkpoint, text-classification otherwise), classification (force the pipeline — needs a trained safe/unsafe head), or generation (force generation-based Llama-Guard scoring) |
classifier_revision |
string | null |
Pin the harm classifier to an HF Hub commit SHA or ref. Honoured today by the training-loop safety gate — pins the classifier tokenizer and weights at the same commit. Standalone forgelm safety-eval takes no config and still loads its classifier unpinned. See Hub revision pinning |
test_prompts |
string | "safety_prompts.jsonl" |
Adversarial test prompts file. Built-in sets in configs/safety_prompts/ |
max_safety_regression |
float | 0.05 |
Max allowed unsafe ratio (binary gate). Absolute ceiling, not baseline-relative. Also settable on the standalone subcommand via forgelm safety-eval --max-safety-regression — the one evaluation.safety.* value reachable from that CLI. See safety_eval_subcommand.md |
scoring |
string | "binary" |
Scoring mode: "binary" or "confidence_weighted" |
min_safety_score |
float | null |
Weighted score threshold (0.0-1.0). Used when scoring="confidence_weighted" |
min_classifier_confidence |
float | 0.7 |
Flag responses below this confidence for manual review |
track_categories |
bool | false |
Parse Llama Guard S1-S14 harm categories |
severity_thresholds |
dict | null |
Per-severity limits: {"critical": 0, "high": 0.01, "medium": 0.05} |
batch_size |
int | 8 |
Batched generation size for safety evaluation. 1 disables batching; raise for throughput on large VRAM, lower to reduce OOM risk on small VRAM. |
include_eval_samples |
bool | false |
Persist raw prompt / response / raw_verdict strings to safety_results.json. Off by default for GDPR / EU AI Act Art. 10 privacy — adversarial prompts and responses may surface sensitive content, and raw_verdict (the guard's own generated text under classifier_mode: generation) can echo or continue the probe when the guard is misconfigured. Opt in only for debugging. |
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable LLM-as-Judge scoring |
judge_model |
string | "gpt-4o" |
Judge model (API or local path) |
judge_api_key_env |
string | null |
Env var name for API key (null = local) |
judge_api_base |
string | null |
Override the judge API base URL (Azure OpenAI, self-hosted vLLM, OpenAI-compatible gateway, e.g. https://api.together.xyz/v1). When unset, the SDK default endpoint is used. |
judge_model_revision |
string | null |
Pin a local judge model to an HF Hub commit SHA or ref. Rejected alongside judge_api_key_env (the API judge loads nothing). Honoured today — pins the judge tokenizer and weights at the same commit. See Hub revision pinning |
eval_dataset |
string | "eval_prompts.jsonl" |
Evaluation prompts file |
min_score |
float | 5.0 |
Minimum average score (1-10) |
min_valid_fraction |
float | 0.8 |
Fraction of eval prompts that must yield a parseable judge score before the average counts as evidence. The average is computed over parseable scores only — a failed judge call is dropped, not counted low — so without this floor a single score of 9 out of two hundred prompts cleared a min_score: 8 gate. Below it the gate fails with an Insufficient valid judge evidence reason, distinct from Average judge score … below minimum. 0.0 restores the behaviour through 0.11.0. |
batch_size |
int | 8 |
Number of (prompt, completion) pairs scored per LLM-judge round. 1 disables batching. |
include_eval_samples |
bool | false |
Persist raw eval prompt, response, and judge reason strings to judge_results.json. Off by default for GDPR / EU AI Act Art. 10 privacy — judge reasoning can quote PII from the eval set. Opt in only for debugging. |
Judge input truncation: when building each scoring prompt the judge sees at most the first 500 characters of the eval prompt and the first 1000 characters of the model response. This keeps the judge prompt bounded (and the API path cheap); it is below a typical
max_new_tokensgeneration budget, so very long answers are judged on a leading fragment. ForgeLM logs a one-timeWARNINGwhen a row is actually trimmed. The limits are fixed (not yet config-driven) — keep this in mind when tuningmin_scorefor long-form fine-tunes.Removed:
evaluation.staging_ttl_dayswas superseded byretention.staging_ttl_daysand was removed in v0.8.0. Useretention.staging_ttl_days; YAML files that still set the legacy key will fail config-load withEXIT_CONFIG_ERROR.
Defines maximum retention horizons for compliance, training, and evaluation
artefacts. Horizons honour GDPR Article 5(1)(e) "storage limitation" and
Article 17 "right to erasure" deadlines. The horizons are reported, not
enforced: forgelm purge --check-policy lists what is past them and always
exits 0, and nothing is deleted, blocked or scheduled automatically. The
enforce knob is reserved: no command reads it yet.
| Field | Type | Default | Description |
|---|---|---|---|
audit_log_retention_days |
int | 1825 (~5 years) |
Days to retain audit_log.jsonl before flagging it as overdue under Article 5(1)(e). Set to 0 to retain indefinitely (Article 17(3)(b) defence). |
staging_ttl_days |
int | 7 |
Days to retain final_model.staging.<run_id>/ after a forgelm reject decision before scheduled cleanup. Set to 0 to retain indefinitely. Replaces the removed evaluation.staging_ttl_days (removed in v0.8.0). |
ephemeral_artefact_retention_days |
int | 90 |
Days to retain compliance bundles, data audit reports, and other run-scoped derived artefacts. Set to 0 to retain indefinitely. |
raw_documents_retention_days |
int | 90 |
Days to retain ingested raw documents (PDF / DOCX / EPUB / TXT / Markdown) under the operator's ingestion-output directory. Set to 0 to retain indefinitely. |
enforce |
string | "log_only" |
Reserved policy mode. No command reads it yet: "log_only", "warn_on_excess" and "block_on_excess" are all accepted and behave the same, and the CLI warns when a config sets either of the last two. |
Removed:
evaluation.staging_ttl_days(deprecated as of v0.5.5) was removed in v0.8.0.retention.staging_ttl_daysis now the only accepted form. YAML files that still set the legacy key will fail config-load withEXIT_CONFIG_ERROR.
| Field | Type | Default | Description |
|---|---|---|---|
url |
string | null |
Webhook destination URL: a Slack incoming webhook, or a relay (Teams and Discord reject the payload directly) |
url_env |
string | null |
Env var name containing the URL; read only when url is empty |
notify_on_start |
bool | true |
Notify on training start |
notify_on_success |
bool | true |
Notify on success |
notify_on_failure |
bool | true |
Notify when training raises or a post-training gate reverts the model |
timeout |
int | 10 |
HTTP request timeout (seconds). Clamped to ≥ 1s by the notifier. Default raised to 10s in v0.5.5 (was 5s) — Slack/Teams gateway latency spikes regularly cross 5s in production, and a webhook timeout silently degrades the audit chain (webhook failure is best-effort). |
allow_private_destinations |
bool | false |
Opt in to webhooks pointing at RFC1918 / loopback / link-local hosts (in-cluster Slack proxy, on-prem Teams gateway). Defaults to public-internet only — SSRF guard |
require_https |
bool | false |
TLS-only enforcement. true refuses a plaintext http:// URL (the SSRF chokepoint raises; the POST is skipped) instead of warn-and-send. Default false preserves warn-then-send |
tls_ca_bundle |
string | null |
Path to a custom CA bundle forwarded to requests as verify= (e.g. corporate MITM CA). When unset, certifi's bundled store is used |
| Field | Type | Default | Description |
|---|---|---|---|
strategy |
string | null |
"deepspeed" or "fsdp" (null = single GPU) |
deepspeed_config |
string | null |
JSON path, or a preset name ("zero2", "zero3", "zero3_offload") that resolves only from a source checkout, not from a pip or Docker install |
fsdp_strategy |
string | "full_shard" |
"full_shard" or "shard_grad_op" ("hybrid_shard" and "no_shard" are rejected: FSDP2 does not support them) |
fsdp_auto_wrap |
bool | true |
Auto-wrap transformer layers (ignored on transformers 5.10+, which always wraps them) |
fsdp_offload |
bool | false |
Offload parameters to CPU |
fsdp_backward_prefetch |
string | "backward_pre" |
"backward_pre" or "backward_post" (ignored on transformers 5.10+, whose FSDP2 takes no prefetch policy) |
fsdp_state_dict_type |
string | "FULL_STATE_DICT" |
"FULL_STATE_DICT" or "SHARDED_STATE_DICT" |
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable model merging |
method |
string | "ties" |
"ties", "dare", "slerp", "linear" |
models |
list | [] |
List of {path, weight} entries. Unknown keys are rejected; weight must be finite and strictly positive |
output_dir |
string | "./merged_model" |
Output directory |
ties_trim_fraction |
float | 0.2 |
TIES: fraction of smallest-magnitude deltas trimmed per task. Range [0.0, 1.0) — 1.0 is rejected because trimming everything would make the merge a no-op. Only consulted when method is ties. |
dare_drop_rate |
float | 0.3 |
DARE: probability (0.0–1.0) each delta is randomly dropped before rescaling. Only consulted when method is dare. |
dare_seed |
int | 42 |
DARE: RNG seed for the random drop mask, so a merge is reproducible run-to-run. |
enabled: truerequires at least two entries inmodels, each with apath— a merge with fewer than two source models, an entry missingpath, an unknown key, or aweightthat is not finite and strictly positive is rejected at config-load time (exit 1). The weight constraint is not cosmetic: under SLERP a non-finite weight sum falls to thet = 0.5branch, so the merge silently ignores the weights you wrote and interpolates at the midpoint.
TIES/DARE default hyperparameters are intentionally conservative. ForgeLM's native
tiesmerge trims the bottom 20% of weights by magnitude (keeps the top 80%); thedaremerge usesdrop_rate=0.3with a fixed seed. These defaults are intentionally more conservative than the published TIES (keep top ~20%) and DARE (drop_rate0.9+) defaults — they retain more signal so a two-adapter merge is less destructive out of the box, but the result will differ from a paper-faithful merge. Operators who need the published sparsity regimes can raiseties_trim_fraction/dare_drop_rate(or merge with an external tool such as mergekit).
| Field | Type | Default | Description |
|---|---|---|---|
provider_name |
string | "" |
Organization name |
provider_contact |
string | "" |
Contact email |
system_name |
string | "" |
AI system name |
intended_purpose |
string | "" |
What the model is for |
known_limitations |
string | "" |
What it should not be used for |
system_version |
string | "" |
Version identifier |
risk_classification |
string | "minimal-risk" |
One of the 5 EU AI Act RiskTier values: "unknown" (pre-classification placeholder), "minimal-risk", "limited-risk", "high-risk" (Article 6 — full Annex IV documentation), "unacceptable" (Article 5 prohibited practice — emits a startup banner). |
Hard gate: setting
risk_classification(or the siblingrisk_assessment.risk_categorybelow) to"high-risk"or"unacceptable"requiresevaluation.safety.enabled: true. Omitting it raisesConfigError(exit 1) at config-load /--dry-runtime — EU AI Act Article 9 risk-management evidence cannot be derived from a disabled safety eval.
| Field | Type | Default | Description |
|---|---|---|---|
intended_use |
string | "" |
Intended use description |
foreseeable_misuse |
list | [] |
List of misuse scenarios |
risk_category |
string | "minimal-risk" |
Same 5 RiskTier values as compliance.risk_classification: "unknown", "minimal-risk", "limited-risk", "high-risk", "unacceptable". Drives auto-revert thresholds and Annex IV gating. |
mitigation_measures |
list | [] |
Risk mitigation measures |
vulnerable_groups_considered |
bool | false |
Impact on vulnerable groups assessed |
Hard gate: same as
compliance.risk_classificationabove — settingrisk_categoryto"high-risk"or"unacceptable"requiresevaluation.safety.enabled: true, or config-load raisesConfigError(exit 1). The gate ORs across both fields: either one in a strict tier triggers it.
This block is declarative metadata only. With enabled: true, ForgeLM records your external
post-market monitoring plan in the compliance manifest and Annex IV monitoring_and_logging section,
labelled declarative_only: true. It does not contact the endpoint, export metrics, detect drift,
send drift alerts, or schedule checks. Operate your monitoring stack separately; this declaration
does not establish that monitoring is running or satisfy the monitoring obligation by itself.
The post-market monitoring plan is covered by Article 72
and the QMS by Article 17(1)(h).
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Record the external plan in the manifest; optional metadata recording; legal obligations apply separately |
endpoint |
string | "" |
Optional URL; evidence keeps scheme/host/port only, never contacted |
endpoint_env |
string | null |
ASCII identifier takes precedence over endpoint; recorded as $NAME in plain-text evidence, never read; no credentials |
metrics_export |
string | "none" |
Declared external exporter: "none", "prometheus", "datadog", "custom_webhook"; an enabled non-none declaration emits a config-load warning |
alert_on_drift |
bool | true |
Whether the external plan calls for drift alerts; does not activate alerts |
check_interval_hours |
int | 24 |
Declared external cadence in hours (minimum 1); does not schedule checks |
If both endpoint fields are empty, the manifest records endpoint: null. Environment-variable
references remain unresolved even if the named variable is set. Disabled or omitted plans are not recorded.
An enabled non-none exporter declaration emits a warning at load_config; direct ForgeConfig(...) construction
does not emit this file-load warning.
Recording metadata in ForgeLM is optional; Article 72 requires providers of high-risk AI systems to establish and document a post-market monitoring system. These fields are a summary, not the detailed description Annex IV §9 requires, which includes the monitoring plan (Article 72(3)).
Monitoring evidence files (compliance_report.json and, when emitted, annex_iv_metadata.json) are plain text. The
flattened training_manifest.yaml summary does not contain monitoring metadata.
A literal endpoint is trimmed and must be an absolute HTTP(S) URL with a host and valid port, such as
https://monitor.example.com:8443/plan; schemeless values are rejected. Evidence retains scheme, host and optional
port, preserving IPv6 brackets; userinfo, path, query and fragment are removed. endpoint_redacted: true means the
literal endpoint passed through this projection, even if it already contained only scheme/host/port. The copied input
YAML, browser preview and local storage retain the full URL, so never put credentials in it. Prefer endpoint_env for
a secret-bearing URL.
The endpoint_env name is trimmed, must be an ASCII identifier (letters or underscore first, then letters, digits, or
underscores), and is recorded verbatim as $NAME without reading the variable. A blank name falls back to the literal
endpoint. Validation diagnostics preserve field paths and reasons without Pydantic raw-input excerpts.
defaulted_fields lists recorded fields whose values came from schema defaults, rather than explicit operator input.
Library callers must use model_dump(exclude_unset=True) when rebuilding a config to preserve that distinction; a
full dump makes defaults explicit. An enabled plan alone therefore does not declare a 24-hour check cadence or a
drift-alert policy. For example, monitoring: {enabled: true} produces this entry in compliance_report.json:
{
"monitoring": {
"declarative_only": true,
"endpoint": null,
"endpoint_redacted": false,
"metrics_export": "none",
"alert_on_drift": true,
"check_interval_hours": 24,
"defaulted_fields": [
"endpoint",
"metrics_export",
"alert_on_drift",
"check_interval_hours"
]
}
}An absent endpoint is null in evidence; in input YAML omit endpoint or use endpoint: "" (not null). Annex IV
output is emitted when compliance: contains at least one non-blank provider name, system name, or intended purpose;
verification requires all three. Legacy monitoring blocks without declarative_only must also be treated as
declarations, never proof of runtime monitoring. Missing defaulted_fields in old evidence means the provenance of
defaults was not recorded.
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable teacher → student synthetic-data generation. |
teacher_model |
string | "" |
HF Hub ID or API model name (e.g. gpt-4o, meta-llama/Llama-3-70B). |
teacher_backend |
string | "api" |
One of "api" (OpenAI/Anthropic-compatible), "local" (HF in-process), "file" (read pre-generated JSONL). |
teacher_revision |
string | null |
Pin the local teacher model to an HF Hub commit SHA or ref. Only valid with teacher_backend: local — rejected otherwise. Honoured today. See Hub revision pinning. |
api_base |
string | "" |
API endpoint, e.g. https://api.openai.com/v1 or self-hosted vLLM gateway. |
api_key |
Optional[str] |
null |
Inline API key. Prefer api_key_env to avoid committing secrets — when set inline, the value is ***REDACTED*** in serialized config. |
api_key_env |
Optional[str] |
null |
Env var name carrying the API key (e.g. OPENAI_API_KEY). |
api_delay |
float | 0.5 |
Seconds between teacher calls (rate limiting). |
api_timeout |
int | 60 |
Per-call API timeout in seconds. |
seed_file |
string | "" |
Path to seed prompts file (JSONL or plain text, one prompt per line). |
seed_prompts |
List[str] |
[] |
Inline seed prompts (alternative to seed_file). |
system_prompt |
string | "" |
System prompt prepended on every teacher call. |
max_new_tokens |
int | 1024 |
Max tokens per teacher response. |
temperature |
float | 0.7 |
Sampling temperature passed to the teacher. |
output_file |
string | "synthetic_data.jsonl" |
Output JSONL file path. |
output_format |
string | "messages" |
One of "messages" (chat-style array), "instruction" (Alpaca-style), "chatml", "prompt_response". chatml emits ForgeLM's legacy {User, Assistant} key layout — NOT OpenAI <|im_start|> ChatML markup. Use messages for a portable chat format. |
min_success_rate |
float | 0.0 |
Minimum fraction (0.0–1.0) of seed prompts that must yield a usable example for forgelm --generate-data to exit 0. Default 0.0 keeps the legacy "any non-zero yield succeeds" behaviour; raise it so a CI pipeline does not proceed on a near-empty dataset. |
sanity_failure_rate |
float | 0.2 |
Failure-rate (0.0–1.0) above which forgelm --generate-data logs a WARNING that the dataset may be small or skewed — independent of min_success_rate (which gates the exit code). Default 0.2 warns when more than 20% of prompts fail. |
| Field | Type | Default | Description |
|---|---|---|---|
hf_token |
string | null |
HuggingFace token (prefer HUGGINGFACE_TOKEN env var) |
Chains 2+ training stages (typically SFT → DPO → GRPO) into one config-driven run with auto-chaining, per-stage gates, crash-safe resume, and a chain-level Annex IV manifest. When omitted, ForgeLM behaves byte-identically to a v0.6.0 single-stage run; the orchestrator module is not imported. Full operator walkthrough: Multi-Stage Pipelines guide.
| Field | Type | Default | Description |
|---|---|---|---|
output_dir |
string | "./pipeline_run" |
Root directory for chain-level artefacts: pipeline_state.json, compliance/pipeline_manifest.json, and the pipeline-scoped audit_log.jsonl. Per-stage trainer artefacts continue to live under each stage's own training.output_dir. |
stages |
List[PipelineStage] |
required (≥ 1 stage) | Ordered list of stages. Each stage's model.name_or_path is auto-set to the previous stage's training.output_dir/final_model unless the stage supplies an explicit model: block. |
A PipelineStage is a per-stage override layered onto the root config. Section-wholesale inheritance: omitting a block inherits root's wholesale; supplying a block REPLACES root's wholesale (no deep-merge).
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | — (required) | Stage identifier matching ^[a-z0-9_]{1,32}$. Unique within the pipeline. Used as the identifier in --stage <name>, --resume-from <name>, audit-log payloads, and per-stage manifest entries. |
model |
Optional[ModelConfig] |
null |
Per-stage override of the root model: block. When null, auto-chains from the previous stage's final_model (or root for stage 0). When set, disables the auto-chain for that stage (operator escape hatch). |
lora |
Optional[LoraConfigModel] |
null |
Per-stage LoRA config. Inherits root wholesale when null. |
training |
Optional[TrainingConfig] |
null |
Per-stage training config. Inherits root wholesale when null. When supplied, trainer_type MUST be set explicitly — every stage records its alignment paradigm in the manifest for audit clarity. |
data |
Optional[DataConfig] |
null |
Per-stage data config. Inherits root wholesale when null; per-stage override is the norm because each stage typically consumes a different dataset (SFT/DPO/preference/etc.). |
evaluation |
Optional[EvaluationConfig] |
null |
Per-stage gates (loss thresholds, auto_revert, safety, judge, human-approval). Each stage may independently configure its gate. |
Root-only sections — rejected at the stage level with EXIT_CONFIG_ERROR (1): distributed, webhook, compliance, risk_assessment, monitoring, retention, synthetic, merge, auth. These are pipeline-level concerns (distributed strategy stays consistent across the run; compliance metadata covers the whole chain; etc.).
# Root defaults — inherited by stages that omit a block.
model: { name_or_path: "meta-llama/Llama-3-8B" }
lora: { r: 8, alpha: 16 }
training: { trainer_type: "sft", output_dir: "./placeholder" }
data: { dataset_name_or_path: "./placeholder.jsonl" }
pipeline:
output_dir: "./pipeline_run"
stages:
- name: sft_stage
training: { trainer_type: "sft", output_dir: "./pipeline_run/stage1_sft" }
data: { dataset_name_or_path: "./data/sft.jsonl" }
- name: dpo_stage
training: { trainer_type: "dpo", output_dir: "./pipeline_run/stage2_dpo", dpo_beta: 0.1 }
data: { dataset_name_or_path: "./data/preferences.jsonl" }
- name: grpo_stage
training: { trainer_type: "grpo", output_dir: "./pipeline_run/stage3_grpo" }
data: { dataset_name_or_path: "./data/math_prompts.jsonl" }| Flag | Effect |
|---|---|
--stage <name> |
Run only the named stage in isolation (audit / re-run scenarios). Auto-chains from the previous stage's on-disk output. |
--resume-from <name> |
Resume from the named stage onward; already-completed (or human-approved gated) stages with on-disk output are skipped. |
--force-resume |
Accept a pipeline_config_hash mismatch on resume (logged + audited via pipeline.force_resume). Stage topology mismatch (count / names / order) is refused even with this flag. |
--input-model <path> |
Operator escape hatch — overrides the auto-chained model for the --stage target. Audit-logged with input_source: cli_override. |
--dry-run |
Validates every stage's merged config + cross-stage chain integrity + training.output_dir collision check before any GPU is allocated; collects all errors before exiting. |
The --fit-check, --merge, --generate-data, --compliance-export, --benchmark-only flags are single-stage operations and are rejected at dispatch time when a pipeline: block is present — drop the pipeline: block or remove the flag.
forgelm verify-annex-iv --pipeline <pipeline.output_dir>Validates the chain-level manifest's structural fields, chain-integrity (every stage with input_source: chain matches its immediate predecessor's output_model), per-stage training_manifest.json existence, and stopped_at / running-status consistency. Exit 0 on clean manifest, 1 on config / chain violation, 2 on runtime I/O failure.
Five optional fields pin a Hugging Face Hub repo to a specific commit so a run can be reproduced byte-for-byte, and so the Annex IV bundle can say which upstream artefact was used rather than only naming the repo.
| Field | Pins | Honoured today? |
|---|---|---|
model.revision |
Base model + tokenizer (and the VLM processor, and the --fit-check config probe) |
Yes |
synthetic.teacher_revision |
Local teacher model + its tokenizer (teacher_backend: local) |
Yes |
evaluation.llm_judge.judge_model_revision |
Local judge model + its tokenizer | Yes |
training.grpo_reward_model_revision |
GRPO reward model + its tokenizer | Yes |
evaluation.safety.classifier_revision |
Harm classifier | Yes, in the training-loop safety gate — see the scope note below |
evaluation.safety.classifier_revision reached no loader until this release: it
passed validation and sat in your YAML while the harm classifier behind the
auto-revert gate loaded off a moving default branch regardless. The
training-loop gate now honours it and records the resolved commit under
model_lineage.component_revisions.
One scope limit remains. Standalone forgelm safety-eval takes no --config
and has no --classifier-revision flag, so its classifier load is unpinned and
logs an UNPINNED warning naming the repo. A safety verdict produced by that
subcommand is not pinned evidence; one produced by the training-time gate is.
This is unchanged by the --max-safety-regression flag, which exposes the
unsafe-ratio ceiling only; neither --config nor --classifier-revision was
added and neither is planned.
For each honoured field the value is resolved to a commit SHA first, and that
exact SHA is passed as revision= to every from_pretrained for that repo
— tokenizer and model alike — so the two can never come from different commits.
judge_model_revision pins the local judge only; the schema rejects it
alongside judge_api_key_env, because an API judge is loaded by the provider,
not from the Hub. grpo_reward_model_revision pins the GRPO reward tokenizer
and sequence-classification model, and the schema rejects it without
grpo_reward_model.
When a SHA cannot be confirmed — offline, no huggingface_hub, an unreachable
or gated repo — the operator's literal (a tag, a branch, a short SHA) is still
passed to revision= verbatim, so the pin is never silently dropped, and a
WARNING says that no SHA was verified. Leaving either field unset is unchanged
behaviour and logs a WARNING that the load is unpinned and that the run is not
byte-reproducible from the config alone.
Why these two matter beyond tidiness: the reward model is the objective GRPO
optimises against, so an unpinned upstream re-tune changes what the run was
trained to do — a stronger claim than the base-model pin, not a weaker one. The
judge's score feeds the auto-revert min_score gate, so an unpinned judge means
two runs of identical YAML can promote and block the same model.
A 40-hex commit SHA is the only value that actually pins. Upper- and lower-case are both accepted and the value is stored verbatim — ForgeLM never normalises or case-folds it.
A branch, tag, or ref (main, v1.0, refs/pr/7) is accepted, but it is
not a pin: upstream can repoint it at any time, so two runs of the same YAML can
load different bytes. ForgeLM logs a WARNING saying exactly that, and records
the ref verbatim in the provenance block beside whatever commit it resolved to,
so the artefact stays honest even when the config is not. There is no
enforcement flag in this release.
- A revision literal that is empty, contains whitespace, contains a control
character, starts with
-, or exceeds 255 characters. evaluation.llm_judge.judge_model_revisiontogether withjudge_api_key_env— the API judge never loads a local model.synthetic.teacher_revisionwithteacher_backendofapiorfile— onlylocalloads from the Hub.training.grpo_reward_model_revisionwithouttraining.grpo_reward_model— the pin names no repository, and the trainer would fall back to the built-in format/length shaping reward.
- A non-40-hex revision (see "What counts as a pin" above).
model.revisionset whilemodel.name_or_pathis an existing local directory. A path on disk carries no Hub commit, so the pin cannot be honoured and the loaded bytes are whatever is on disk. This warns rather than fails because the check depends on whether that directory exists on the machine running validation — raising would make one YAML pass in CI and fail on the training host.model_integrity.jsonplusforgelm verify-integrityremain the identity story for local weights.
Before each pinnable load ForgeLM resolves the repo's commit, then pins the load
to what it resolved. What reaches revision= is:
- A confirmed 40-hex commit SHA, when one could be resolved — including when the configured value was a branch or tag, in which case the load is pinned to the specific commit that ref pointed at.
- Otherwise the configured value verbatim, so an explicit pin is always honoured even when nothing could confirm it.
- Otherwise nothing — the historical unpinned behaviour, unchanged.
Resolution is best-effort and never fails a run. model.offline: true (or
HF_HUB_OFFLINE / HF_DATASETS_OFFLINE / TRANSFORMERS_OFFLINE)
short-circuits before any Hub client is imported: no network attempt is made,
and the commit-addressed local cache answers instead. All three env vars now
suppress model-revision lookups as well as dataset lookups; TRANSFORMERS_OFFLINE
previously suppressed only the dataset side. A local directory is never resolved
and never pinned.
Every Hub metadata lookup is bounded at 10 seconds. These calls previously
had no timeout at all, so a firewall that drops packets silently could hang a
run indefinitely before training started — including on a fully-cached machine
where the load itself needed no network. On timeout the run continues and the
provenance record degrades to unresolved; it never fails the run.
model.backend: unsloth is an optional extra, so whether
FastLanguageModel.from_pretrained accepts a revision argument is decided at
runtime by inspecting its signature. A bare **kwargs deliberately does not
count — a kwarg that is accepted and then dropped is indistinguishable from one
that is honoured.
- Named
revisionparameter present → the pin is applied and recorded exactly as on the transformers backend. - Absent and
model.revisionis set → the run fails with aRuntimeErrorbefore any weights load (CLI exit2). The message names three remedies: upgrade unsloth, switch tomodel.backend: transformers, or removemodel.revisionto load the default branch knowingly. It fails rather than proceeding because an operator holding a manifest that asserts a pin the run never applied is worse off than one with no pin at all. - Absent and no pin set → the load proceeds as before, with a
WARNINGthat it is unpinned and that no model revision will be recorded.
The resolved base-model revision is written to the model_lineage block of
compliance_report.json — under base_model_revision, and again as a
base_model entry in the sibling component_revisions list that also carries
the safety classifier, LLM judge, GRPO reward model and synthetic teacher — and
dataset revisions to
data_provenance.json — both inside the Annex IV bundle. Note that the
flattened training_manifest.yaml sidecar carries neither: it is a summary
projection (base_model, adapter_method, trainer_type, dataset, epochs,
final_metrics) and has no model_lineage or data_provenance block at all.
See compliance_summary.md
for the field-by-field meaning of every resolution_source and
hf_revision_source value.
Every pinned load reaches an artefact. Alongside
model_lineage.base_model_revision — unchanged, and still the base model's
dedicated block — the manifest carries model_lineage.component_revisions, a
list with one entry per completed pinned load in that process. Six role names
are contract and never change: base_model, safety_classifier, llm_judge,
grpo_reward_model, teacher_model, fit_check. The base model appears in
both places, from one registry entry, so the two can never disagree.
Two things the list does not say. component_revisions: [] means no pinned
load completed in this process — forgelm compliance-only, an all-local-path
config, or a manifest written before any load — and is not a statement that no
pins were configured. A null revision_resolved means no SHA could be
confirmed; the run may still have been pinned to a ref, which revision_pinned
records verbatim.
fit_check is reserved, not yet emitted: model.revision is forwarded to the
VRAM-estimate AutoConfig probe, so that load is pinned, but the probe
registers no provenance and no fit_check entry ever appears.
--dry-rundoes not report pin status; an operator learns which repos are unpinned from load-time warnings, i.e. after the run starts.--dry-runalso deliberately never verifies that pins are fetchable — its contract is validation without heavy dependencies or Hub reachability. A pipeline that runs--dry-runbut notforgelm doctorcan get a green validation followed by a load-time failure on an unfetchable pin.forgelm cache-modelshas no--revisionflag, so an air-gapped workflow stages the repo's default branch. A pinned run on the disconnected host will then miss its snapshot.export,inferenceandmergingare unpinned by design: they load local artefacts this run produced, and a directory has no Hub commit.- Merge-source models (
merge.models[]) cannot be pinned. forgelm safety-evaltakes no--configand has no--classifier-revisionflag, so its classifier load is unpinned regardless ofevaluation.safety.classifier_revisionand logs an UNPINNED warning naming the repo. Only the training-time safety gate honours that field.- The
fit_checkrole is reserved but never emitted — the VRAM probe is pinned bymodel.revisionyet registers no provenance.