Gregory-L's picture
v1.0.2 — the Hub is reachable from the command line, and is not a chain
c4e0c8c verified
Raw
History Blame Contribute Delete
42.1 kB
"""mindxtrain CLI — Typer entry point for all 8 verbs."""
from __future__ import annotations
from pathlib import Path
import typer
from rich.console import Console
from mindxtrain import __version__
from mindxtrain.autotune.benchmark import run_autotune
from mindxtrain.autotune.plan import AutotunePlan
from mindxtrain.config.loader import list_recipes, load_config, render_recipe
app = typer.Typer(
name="mindxtrain",
help="mindxtrain: 60s AOT autotune + multi-backend training + Quark FP8 quantize for MI300X.",
no_args_is_help=True,
)
dataset_app = typer.Typer(name="dataset", help="Dataset preparation subcommands.", no_args_is_help=True)
github_app = typer.Typer(name="github", help="GitHub source-tree publishing.", no_args_is_help=True)
droplet_app = typer.Typer(name="droplet", help="AMD Dev Cloud MI300X provision + sync.", no_args_is_help=True)
mei_app = typer.Typer(
name="mei",
help="mindX Efficiency Index — score, history, promotion checks.",
no_args_is_help=True,
)
hf_app = typer.Typer(
name="hf",
help="Hugging Face — who the token is, warm a base, publish a generation, read the lineage.",
no_args_is_help=True,
)
app.add_typer(dataset_app)
app.add_typer(github_app)
app.add_typer(droplet_app)
app.add_typer(mei_app)
app.add_typer(hf_app)
console = Console()
def _version_cb(value: bool) -> None:
if value:
console.print(f"mindxtrain {__version__}")
raise typer.Exit
@app.callback()
def _root(
version: bool = typer.Option(
False,
"--version",
callback=_version_cb,
is_eager=True,
help="Print version and exit.",
),
) -> None:
"""mindxtrain entry point."""
# ---- init / bench ---------------------------------------------------------
@app.command()
def init(
template: str = typer.Option(
"qwen3_8b_sft_lora",
"--template",
"-t",
help="recipe name (run `mindxtrain init --list` to see all)",
),
out: Path = typer.Option(
Path("run.yaml"),
"--out",
"-o",
help="output YAML path",
),
list_only: bool = typer.Option(
False,
"--list",
help="list all built-in recipe names and exit",
),
) -> None:
"""Write a starter YAML config from a built-in recipe."""
if list_only:
for name in list_recipes():
console.print(f" {name}")
raise typer.Exit
yaml_text = render_recipe(template)
out.write_text(yaml_text)
console.print(f"[green]wrote[/green] {out} ({len(yaml_text)} bytes, recipe={template!r})")
@app.command()
def bench(
out: Path = typer.Option(Path("autotune_plan.json"), "--out", "-o"),
gpu: int = typer.Option(0, "--gpu", help="HIP/ROCm device index"),
dry_run: bool = typer.Option(
False,
"--dry-run",
help="Skip GPU probes; emit a hardcoded reference plan.",
),
) -> None:
"""Run the 60-second AOT autotune probe and write autotune_plan.json."""
plan: AutotunePlan = run_autotune(gpu_index=gpu, dry_run=dry_run)
out.write_text(plan.model_dump_json(indent=2))
console.print(
f"[green]wrote[/green] {out} (dry_run={dry_run}, "
f"attention={plan.attention_backend}, gemm={plan.gemm_heuristic})",
)
# ---- train / eval / quantize / serve --------------------------------------
def _load_plan(plan_path: Path | None) -> AutotunePlan:
if plan_path and plan_path.exists():
return AutotunePlan.model_validate_json(plan_path.read_text())
return run_autotune(gpu_index=0, dry_run=True)
@app.command()
def train(
config: Path = typer.Argument(..., help="path to XTrainConfig YAML"),
plan_path: Path = typer.Option(None, "--plan", help="autotune plan JSON; falls back to dry-run."),
out: Path = typer.Option(Path("./out/runs"), "--out", "-o", help="run output root"),
cpu_percent: int | None = typer.Option(
None, "--cpu-percent",
help=(
"Override `train.cpu_throttle.percent` at runtime. Applies "
"only to the trl_cpu backend. 1-100; below 1 or above 100 errors."
),
),
cpu_nice: int | None = typer.Option(
None, "--cpu-nice",
help="Override `train.cpu_throttle.nice_level`. -20..19.",
),
) -> None:
"""Dispatch a training run via the configured backend.
With --cpu-percent N, the trl_cpu backend caps every thread pool
(torch, OpenMP, MKL, OpenBLAS) at N% of the host's cores. Useful for
leaving cycles free for the rest of the laptop while training runs in
the background.
"""
from mindxtrain.config.schema import CPUThrottleCfg
from mindxtrain.train import dispatch_training
cfg = load_config(config)
# CLI override: rebuild train.cpu_throttle if either knob was passed.
if cpu_percent is not None or cpu_nice is not None:
throttle = cfg.train.cpu_throttle
new_throttle = CPUThrottleCfg(
percent=cpu_percent if cpu_percent is not None else throttle.percent,
nice_level=cpu_nice if cpu_nice is not None else throttle.nice_level,
omp_proc_bind=throttle.omp_proc_bind,
)
# Pydantic frozen=True forbids in-place mutation; rebuild via model_copy.
new_train = cfg.train.model_copy(update={"cpu_throttle": new_throttle})
cfg = cfg.model_copy(update={"train": new_train})
console.print(
f"[dim]cpu_throttle overridden: percent={new_throttle.percent} "
f"nice={new_throttle.nice_level}[/dim]",
)
plan = _load_plan(plan_path)
run_dir = out / cfg.meta.run_name
try:
ckpt = dispatch_training(cfg, plan, run_dir)
except RuntimeError as exc:
console.print(f"[red]training failed:[/red] {exc}")
raise typer.Exit(code=3) from exc
console.print(f"[green]checkpoint:[/green] {ckpt}")
@app.command(name="eval")
def eval_(
config: Path = typer.Argument(...),
checkpoint: Path = typer.Option(None, "--checkpoint", "-c", help="checkpoint dir; default = ./out/runs/<run_name>/checkpoint"),
) -> None:
"""Run lm-eval-harness against a checkpoint."""
from mindxtrain.eval.harness import parse_summary, run_lm_eval
cfg = load_config(config)
ckpt = checkpoint or Path("./out/runs") / cfg.meta.run_name / "checkpoint"
if not ckpt.exists():
console.print(f"[red]checkpoint not found:[/red] {ckpt}")
raise typer.Exit(code=1)
tasks = list(cfg.eval.harness.tasks) if cfg.eval and cfg.eval.harness else ["mmlu"]
try:
results = run_lm_eval(ckpt, tasks)
except RuntimeError as exc:
console.print(f"[red]eval failed:[/red] {exc}")
raise typer.Exit(code=3) from exc
console.print(f"[green]results:[/green] {results}")
console.print_json(data=parse_summary(results))
@app.command(name="eval-checkpoint")
def eval_checkpoint(
config: Path = typer.Argument(...),
checkpoint: Path = typer.Option(
None, "--checkpoint", "-c",
help="LoRA adapter dir; default = ./out/runs/<run_name>/checkpoint",
),
jsonl: Path = typer.Option(
None, "--jsonl",
help=(
"Path to a *_training.jsonl held-out file. Defaults to picking "
"the newest one under `data.path/ltm/**/*_training.jsonl` "
"(works for source='mindx_dreams')."
),
),
max_samples: int = typer.Option(
32, "--max-samples", "-n",
help="Cap on how many rows to evaluate. Held-out CE is averaged.",
),
) -> None:
"""Compare base-model vs base+adapter cross-entropy on held-out chat rows.
The training trainer_state.json gives a train-loss curve but doesn't
answer whether the adapter generalises — this verb does. Prints
`{base_loss, adapter_loss, delta}` where `delta < 0` means the
adapter is actually moving the model toward the held-out dreams.
The default jsonl picker walks the recipe's `data.path` for the
newest `*_training.jsonl` — for mindX dreams this is the corpus the
recipe trained on. For a rigorous held-out check, point `--jsonl` at
a file the training run never saw.
"""
from mindxtrain.eval.held_out_loss import score_checkpoint
cfg = load_config(config)
ckpt = checkpoint or Path("./out/runs") / cfg.meta.run_name / "checkpoint"
if not ckpt.exists():
console.print(f"[red]adapter dir not found:[/red] {ckpt}")
raise typer.Exit(code=1)
jsonl_path = jsonl
if jsonl_path is None:
if cfg.data.source != "mindx_dreams" or cfg.data.path is None:
console.print(
"[red]--jsonl required when data.source != mindx_dreams "
"(no default picker for non-dream sources).",
)
raise typer.Exit(code=2)
candidates = sorted(
Path(cfg.data.path).glob("ltm/**/*_training.jsonl"),
key=lambda p: p.stat().st_mtime,
reverse=True,
)
if not candidates:
console.print(f"[red]no *_training.jsonl under {cfg.data.path}/ltm")
raise typer.Exit(code=2)
jsonl_path = candidates[0]
console.print(f"[dim]using newest dream file: {jsonl_path}[/dim]")
try:
score = score_checkpoint(
adapter_dir=ckpt,
base_model=cfg.model.name,
jsonl_path=jsonl_path,
max_samples=max_samples,
sink=lambda line: console.print(line),
)
except (ImportError, ValueError) as exc:
console.print(f"[red]eval-checkpoint failed:[/red] {exc}")
raise typer.Exit(code=3) from exc
verdict = (
"[green]adapter improved[/green]" if score.delta < 0
else "[yellow]adapter regressed[/yellow]"
)
console.print(
f"\n{verdict} on {score.n} held-out rows: "
f"base={score.base_loss:.4f} adapter={score.adapter_loss:.4f} "
f"delta={score.delta:+.4f}",
)
console.print_json(data=score.as_dict())
@app.command()
def quantize(
config: Path = typer.Argument(...),
checkpoint: Path = typer.Option(None, "--checkpoint", "-c"),
) -> None:
"""Quark FP8 / MXFP4 quantize the trained checkpoint."""
from mindxtrain.deploy.quark import quark_fp8, quark_mxfp4
cfg = load_config(config)
ckpt = checkpoint or Path("./out/runs") / cfg.meta.run_name / "checkpoint"
if not ckpt.exists():
console.print(f"[red]checkpoint not found:[/red] {ckpt}")
raise typer.Exit(code=1)
out_dir = ckpt.parent / "quantized"
fn = quark_fp8 if cfg.quantize.scheme == "fp8_e4m3" else quark_mxfp4
try:
path = fn(ckpt, out_dir)
except RuntimeError as exc:
console.print(f"[red]quantize failed:[/red] {exc}")
raise typer.Exit(code=3) from exc
console.print(f"[green]quantized:[/green] {path}")
@app.command()
def serve(
config: Path = typer.Argument(...),
checkpoint: Path = typer.Option(None, "--checkpoint", "-c"),
to: str = typer.Option(
"vllm", "--to",
help="Serve target: vllm (default, builds vllm-rocm launch cmd), "
"sglang (builds sglang-rocm launch cmd), or "
"ollama (merges LoRA + calls `ollama create`).",
),
tag: str = typer.Option(
None, "--tag",
help="Ollama tag for `--to ollama`. Defaults to run_name when omitted.",
),
ollama_bin: str = typer.Option(
None, "--ollama-bin",
help="Override the ollama binary path (defaults to PATH lookup).",
),
register_as_fallback: bool = typer.Option(
False, "--register-as-fallback",
help=(
"After --to ollama succeeds, PATCH the new tag into mindX as "
"the local-fallback model (PATCH /v1/config/fallback-model). "
"Best-effort — a failure logs but does NOT fail the push."
),
),
mindx_base_url: str = typer.Option(
None, "--mindx-base-url",
help=(
"Override the mindX base URL for --register-as-fallback. "
"Defaults to MINDXTRAIN_API_BASE_URL env or "
"https://mindx.pythai.net."
),
),
) -> None:
"""Serve the trained checkpoint locally.
`--to vllm` (default) prints a vllm-rocm launch command against the
quantized checkpoint — wire it into your orchestrator.
`--to ollama` runs the local-learning loop: merges the LoRA adapter
into the base weights, writes an ollama Modelfile, and calls
`ollama create <tag>` so the trained model is immediately available
on the loopback (the same backend Coach probes for its chat card).
"""
cfg = load_config(config)
if to == "ollama":
from mindxtrain.deploy.ollama_push import push_to_ollama
# The LoRA adapter is at <run_dir>/checkpoint/ — same location
# the trl_cpu / axolotl backends save to.
adapter_dir = checkpoint or Path("./out/runs") / cfg.meta.run_name / "checkpoint"
if not adapter_dir.exists():
console.print(f"[red]checkpoint not found:[/red] {adapter_dir}")
raise typer.Exit(code=1)
resolved_tag = tag or cfg.meta.run_name
try:
result = push_to_ollama(
base_model=cfg.model.name,
adapter_dir=adapter_dir,
tag=resolved_tag,
sink=lambda line: console.print(line),
ollama_bin=ollama_bin,
register_with_mindx=register_as_fallback,
mindx_base_url=mindx_base_url,
)
except (FileNotFoundError, ImportError) as exc:
console.print(f"[red]push-to-ollama failed:[/red] {exc}")
raise typer.Exit(code=2) from exc
console.print(
f"[green]pushed:[/green] {result.tag} "
f"(merged: {result.merged_dir}, Modelfile: {result.modelfile})",
)
if result.mindx_fallback_swap:
console.print(
f"[green]mindX fallback swapped:[/green] "
f"{result.mindx_fallback_swap.get('previous', '?')} -> "
f"{result.mindx_fallback_swap.get('current', '?')}",
)
return
if to not in ("vllm", "sglang"):
console.print(f"[red]unknown serve target:[/red] {to}")
raise typer.Exit(code=2)
ckpt = checkpoint or Path("./out/runs") / cfg.meta.run_name / "quantized"
if not ckpt.exists():
console.print(f"[red]quantized checkpoint not found:[/red] {ckpt}")
raise typer.Exit(code=1)
if to == "sglang":
from mindxtrain.deploy.sglang_rocm import build_sglang_command
cmd = build_sglang_command(cfg.serve, ckpt)
console.print(f"[green]sglang cmd:[/green] {' '.join(cmd)}")
return
from mindxtrain.deploy.vllm_launcher import build_vllm_command
cmd = build_vllm_command(cfg.serve, ckpt, cfg.quantize)
console.print(f"[green]vllm cmd:[/green] {' '.join(cmd)}")
# Caller can pipe the cmd into their orchestrator; we don't exec by default.
# ---- dataset prep ---------------------------------------------------------
@dataset_app.command("prep")
def dataset_prep(
config: Path = typer.Argument(..., help="path to XTrainConfig YAML"),
out: Path = typer.Option(Path("./out/dataset"), "--out", "-o"),
) -> None:
"""Run the dataset pipeline: curate -> filter -> tokenize -> pack -> shard."""
from mindxtrain.data.curate import load_streaming_dataset
from mindxtrain.data.filter import quality_filter
from mindxtrain.data.pack import emit_shards, pack_sequences
from mindxtrain.data.tokenize import tokenize_stream
cfg = load_config(config)
out.mkdir(parents=True, exist_ok=True)
try:
rows = load_streaming_dataset(cfg.data)
texts = (row.get("text") or row.get("content") or "" for row in rows)
clean = quality_filter(texts)
tokenized = tokenize_stream(clean, cfg.model.name)
packed = pack_sequences(tokenized, cfg.data.seq_len)
shard_dir = emit_shards(packed, out)
except RuntimeError as exc:
console.print(f"[red]dataset prep failed:[/red] {exc}")
raise typer.Exit(code=3) from exc
console.print(f"[green]shards:[/green] {shard_dir}")
# ---- publish / receipt ----------------------------------------------------
@app.command()
def publish(
config: Path = typer.Argument(...),
manifest: Path = typer.Option(..., "--manifest", "-m", help="path to provenance manifest.json"),
skip_hf: bool = typer.Option(False, "--skip-hf"),
skip_pin: bool = typer.Option(False, "--skip-pin"),
force: bool = typer.Option(
False, "--force",
help="Skip the MEI promotion gate. The manifest records promotion_bypassed=true.",
),
) -> None:
"""Push to HF Hub + Lighthouse + register the provenance manifest with the mindX API.
By default this verb consults the historical MEI ledger: if there's a
score for this run_id and it doesn't pass the §8 promotion gates, the
push is refused with the failing-gate reasons surfaced. `--force`
skips the gate (records `promotion_bypassed=true` in the manifest).
"""
from mindxtrain.deploy.api_client import register_with_mindx
from mindxtrain.eval.mei import history as _mei_history
from mindxtrain.eval.mei.score import is_promotable
from mindxtrain.provenance.manifest import Manifest
from mindxtrain.storage.hf_hub import publish_to_hf
from mindxtrain.storage.lighthouse import publish_to_lighthouse
cfg = load_config(config)
m = Manifest.model_validate_json(manifest.read_text())
ckpt_dir = Path("./out/runs") / cfg.meta.run_name / "checkpoint"
# MEI promotion gate. Skip silently when there's no MEI score yet —
# the gate is informational, not mandatory at intake (so existing
# publish flows pre-MEI continue to work). With --force, we proceed
# regardless and stamp the manifest so the bypass is auditable.
mei_entries = [e for e in _mei_history.read_all() if e.run_id == m.run_id]
if mei_entries:
latest = mei_entries[-1]
prior = _mei_history.currently_promoted()
prior_score = (
prior.score if prior is not None and prior.run_id != m.run_id else None
)
ok, reasons = is_promotable(latest.score, prior_promoted=prior_score)
if ok:
console.print(
f"[green]MEI gate:[/green] {latest.score.composite:.3f} ≥ 0.55, "
"all sub-indices ≥ 0.30 — promotable.",
)
elif force:
console.print(
"[yellow]MEI gate failed but --force given; "
"marking promotion_bypassed=true in manifest:[/yellow]",
)
for reason in reasons:
console.print(f" • {reason}")
m.promotion_bypassed = True
m.promotion_bypass_reasons = reasons
else:
console.print("[red]MEI gate refused promotion:[/red]")
for reason in reasons:
console.print(f" • {reason}")
console.print(
"Pass --force to publish anyway (the bypass is recorded "
"in the manifest).",
)
raise typer.Exit(code=4)
elif force:
console.print(
"[yellow]No MEI score on file; --force given. "
"Recommend running `mindxtrain mei score <record.json>` first.[/yellow]",
)
hf_url = ""
if not skip_hf and ckpt_dir.exists():
try:
hf_url = publish_to_hf(ckpt_dir, f"{cfg.meta.run_name}", private=False)
m.hf_repo_id = hf_url
console.print(f"[green]HF:[/green] {hf_url}")
except RuntimeError as exc:
console.print(f"[yellow]hf upload skipped:[/yellow] {exc}")
cid = ""
if not skip_pin and ckpt_dir.exists():
try:
cid = publish_to_lighthouse(ckpt_dir)
m.lighthouse_cid = cid
console.print(f"[green]Lighthouse:[/green] {cid}")
except RuntimeError as exc:
console.print(f"[yellow]lighthouse pin skipped:[/yellow] {exc}")
try:
receipt = register_with_mindx(run_id=m.run_id, hf_url=hf_url, cid=cid)
console.print(f"[green]mindX:[/green] {receipt}")
except (RuntimeError, Exception) as exc:
console.print(f"[yellow]mindX register skipped:[/yellow] {exc}")
manifest.write_text(m.model_dump_json(indent=2))
console.print(f"[green]updated manifest:[/green] {manifest}")
@app.command()
def ui(
host: str = typer.Option("127.0.0.1", help="bind address"),
port: int = typer.Option(7862, help="port"),
share: bool = typer.Option(False, help="also expose a public gradio.live link"),
mcp: bool = typer.Option(True, help="serve the rooms as MCP tools too"),
) -> None:
"""Open the Gradio UI: the whole framework on one surface (Basic / Advanced / Scientific)."""
try:
from mindxtrain.ui import main as _ui_main
except ImportError as exc: # pragma: no cover - depends on the extra
msg = "the UI needs gradio: `uv sync --extra ui`"
raise SystemExit(msg) from exc
_ui_main(host=host, port=port, share=share, mcp=mcp)
@app.command()
def receipt(
manifest: Path = typer.Argument(..., help="path to provenance manifest.json"),
config: Path = typer.Option(None, "--config"),
) -> None:
"""Verify a provenance manifest's BLAKE3 hashes against on-disk artifacts."""
from mindxtrain.provenance.manifest import Manifest
from mindxtrain.provenance.verify import verify_receipt
if not manifest.is_file():
console.print(f"[red]manifest not found:[/red] {manifest}")
raise typer.Exit(code=1)
m = Manifest.model_validate_json(manifest.read_text())
console.print_json(data={"run_id": m.run_id, "blake3": m.blake3.model_dump()})
if config is None:
return
cfg = load_config(config)
run_dir = Path("./out/runs") / cfg.meta.run_name
# A run-emitted manifest snapshots the validated config to
# config.snapshot.yaml and persists the exact AutotunePlan bytes it hashed.
# Prefer those when present; fall back to the user-supplied --config for
# legacy manifests produced by `emit_receipt`.
snapshot = run_dir / "config.snapshot.yaml"
config_yaml_path = snapshot if snapshot.is_file() else config
plan_path = run_dir / "autotune_plan.json"
plan_json = plan_path.read_bytes() if plan_path.is_file() else None
try:
result = verify_receipt(
m,
config_yaml_path=config_yaml_path,
dataset_manifest_path=run_dir / "dataset_manifest.json",
checkpoint_dir=run_dir / "checkpoint",
eval_json_path=run_dir / "eval/lm_eval.json",
plan_json=plan_json,
)
except FileNotFoundError as exc:
console.print(f"[red]missing artifact:[/red] {exc}")
raise typer.Exit(code=1) from exc
console.print_json(data=result)
if not all(result.values()):
raise typer.Exit(code=2)
@app.command()
def imprint(
config: Path = typer.Argument(..., help="recipe whose checkpoint to measure"),
out: Path = typer.Option(Path("./out/runs"), "--out", "-o"),
max_inquiries: int = typer.Option(5, "--n", help="number of recall probes"),
trigger_dream: bool = typer.Option(
False, "--trigger-dream",
help="hand the imprinted actor to mindX's machine.dream 8hr cycle",
),
) -> None:
"""Measure a persona imprint: recall before vs after training.
Poses the script's own user-turns back to the actor, comparing the base
model (before) and the trained adapter (after) against the script's
assistant voice. Prints an ImprintReport; exit 4 if no imprint was detected.
"""
import json as _json
cfg = load_config(config)
run_dir = (out / cfg.meta.run_name) if out.name == "runs" else out
adapter_dir = run_dir / "checkpoint"
if not adapter_dir.exists():
console.print(f"[red]no checkpoint to measure:[/red] {adapter_dir}")
raise typer.Exit(code=1)
# Build inquiries (user-turns) + baseline voice (assistant-turns) from the
# local script the actor trained on. Falls back to default probes.
from mindxtrain.eval.imprint import default_inquiries, probe_recall, score_imprint
inquiries: list[str] = []
baseline: list[str] = []
path = cfg.data.path
if path is not None and Path(path).exists():
files = [Path(path)] if Path(path).is_file() else sorted(Path(path).rglob("*.jsonl"))
for f in files:
for line in f.read_text().splitlines():
line = line.strip()
if not line:
continue
try:
row = _json.loads(line)
except _json.JSONDecodeError:
continue
msgs = row.get("messages", [])
u = next((m["content"] for m in msgs if m.get("role") == "user"), None)
a = next((m["content"] for m in msgs if m.get("role") == "assistant"), None)
if u and len(inquiries) < max_inquiries:
inquiries.append(u)
if a:
baseline.append(a)
if not inquiries:
inquiries = default_inquiries(cfg.meta.project)[:max_inquiries]
console.print(f"[cyan]probing {len(inquiries)} inquiries (before/after)…[/cyan]")
try:
before = probe_recall(cfg.model.name, inquiries, force_cpu=True)
after = probe_recall(cfg.model.name, inquiries, adapter_dir=adapter_dir, force_cpu=True)
except RuntimeError as exc:
console.print(f"[red]imprint probe failed:[/red] {exc}")
raise typer.Exit(code=3) from exc
report = score_imprint(inquiries, before, after, baseline or before)
console.print_json(data=report.model_dump())
if trigger_dream:
from mindxtrain.deploy.api_client import trigger_dream_ingestion
res = trigger_dream_ingestion(
run_id=cfg.meta.run_name,
adapter_dir=str(adapter_dir),
base_model=cfg.model.name,
persona_name=cfg.meta.project,
imprint_delta=report.imprint_delta,
)
console.print(f"[green]dream trigger:[/green] {res}")
if not report.imprinted:
console.print("[yellow]no imprint detected (delta<=0 or no shift)[/yellow]")
raise typer.Exit(code=4)
# ---- research (autoresearch search over one editable file) --------------
@app.command()
def research(
contract: Path = typer.Argument(..., help="Path to the AttemptContract TOML."),
researcher: str = typer.Option("codephreak", "--researcher", help="Researcher id."),
max_attempts: int = typer.Option(10, "--max-attempts", "-n", help="Edits to try."),
log_root: Path = typer.Option(Path("./out/research"), "--log-root", help="Ledger root."),
anchor: bool = typer.Option(
False, "--anchor", help="Anchor the champion lineage on Base (needs --extra chain)."
),
) -> None:
"""Run an autoresearch search: iterate edits on one file, keep iff the metric improves.
Each attempt is fenced to the contract's editable file and committed before measuring,
so the search trail is a sequence of re-checkable git commits recorded in a durable
ledger (`<log-root>/attempts.jsonl`). Losers are `git reset --hard` to the champion.
"""
from mindxtrain.research.search import search_from_contract
try:
result = search_from_contract(
contract, researcher=researcher, max_attempts=max_attempts,
log_root=log_root, do_anchor=anchor,
)
except NotImplementedError as exc:
console.print(f"[yellow]{exc}[/yellow]")
raise typer.Exit(code=2) from exc
except Exception as exc: # ResearchAbort, git failures, etc.
console.print(f"[red]research aborted:[/red] {exc}")
raise typer.Exit(code=1) from exc
console.print(f"[green]{result.summary()}[/green]")
# ---- hugging face (the Hub as mindXtrain uses it) -----------------------------
def _hf_report(result: dict, *, quiet: bool = False) -> None:
"""Print a result dict and exit nonzero when it says `ok: false`.
Every `mindxtrain.hf` function returns `{"ok": bool, ...}` rather than raising, so the exit
code is what makes it scriptable: the ascent loop can warm a base, check `$?`, and refuse to
start a three-hour run that would only discover the missing base at the end.
"""
import json as _json
if not quiet:
console.print_json(_json.dumps(result, default=str))
if not result.get("ok"):
raise typer.Exit(code=1)
@hf_app.command("whoami")
def hf_whoami(
token: str = typer.Option("", "--token", help="Defaults to HF_TOKEN in the environment."),
) -> None:
"""Who the token is — and, separately, the namespaces it can actually WRITE.
Org membership is not write scope; the two are reported apart because assuming they are the
same is how a publish fails after the training finished.
"""
from mindxtrain.hf import account
_hf_report(account(token or None))
@hf_app.command("pull")
def hf_pull(
model_id: str = typer.Argument(..., help="Base model repo id, e.g. HuggingFaceTB/SmolLM2-135M."),
token: str = typer.Option("", "--token"),
allow: list[str] = typer.Option(None, "--allow", help="Glob to restrict the download."),
) -> None:
"""Fetch a base model into the local cache before training needs it."""
from mindxtrain.hf import pull_base
_hf_report(pull_base(model_id, token=token or None, allow_patterns=list(allow) if allow else None))
@hf_app.command("warm")
def hf_warm(
config: Path = typer.Argument(..., help="run.yaml — its model.name is what gets pulled."),
token: str = typer.Option("", "--token"),
) -> None:
"""Pull whatever a run config says it needs, so `train` starts cold-free."""
from mindxtrain.hf import warm
_hf_report(warm(config, token=token or None))
@hf_app.command("publish")
def hf_publish(
run_dir: Path = typer.Argument(..., help="A finished run directory."),
repo_id: str = typer.Argument(..., help="Target model repo, e.g. PYTHAI/mindXascension."),
token: str = typer.Option("", "--token"),
private: bool = typer.Option(False, "--private", help="Create the repo private."),
no_merged: bool = typer.Option(False, "--no-merged", help="Adapter only; skip merged weights."),
dry_run: bool = typer.Option(False, "--dry-run", help="Report what would upload; touch nothing."),
) -> None:
"""Publish a finished run as a model repo: weights, adapter, train.log, Modelfile, card."""
from mindxtrain.hf import publish_generation
_hf_report(publish_generation(run_dir, repo_id, token=token or None, private=private,
include_merged=not no_merged, dry_run=dry_run))
@hf_app.command("lineage")
def hf_lineage(
repo_id: str = typer.Argument(..., help="Repo to scan."),
repo_type: str = typer.Option("model", "--repo-type", help="model | dataset | space."),
local_runs: Path = typer.Option(None, "--local-runs", help="Run root, to list what is unpublished."),
token: str = typer.Option("", "--token"),
) -> None:
"""What is on the Hub for a project, reconciled against local runs."""
from mindxtrain.hf import lineage
_hf_report(lineage(repo_id, repo_type=repo_type, token=token or None, local_runs=local_runs))
@hf_app.command("dataset")
def hf_dataset(
path: Path = typer.Argument(..., help="A corpus folder or one JSONL."),
repo_id: str = typer.Argument(..., help="Target dataset repo."),
token: str = typer.Option("", "--token"),
private: bool = typer.Option(False, "--private"),
path_in_repo: str = typer.Option("", "--path-in-repo"),
) -> None:
"""Push a training corpus to a dataset repo."""
from mindxtrain.hf import push_dataset
_hf_report(push_dataset(path, repo_id, token=token or None, private=private,
path_in_repo=path_in_repo))
@hf_app.command("space")
def hf_space(
folder: Path = typer.Argument(..., help="A Gradio folder (must contain app.py)."),
space_id: str = typer.Argument(..., help="Target Space id."),
token: str = typer.Option("", "--token"),
public: bool = typer.Option(False, "--public", help="Public Space. Never park a write token on one."),
hardware: str = typer.Option("zero-a10g", "--hardware", help="Empty string for the free CPU tier."),
) -> None:
"""Push a Gradio folder as a Space."""
from mindxtrain.hf import push_space
_hf_report(push_space(folder, space_id, token=token or None, private=not public,
hardware=hardware or None))
# ---- github / droplet (source-tree publishing + remote provision) -------
@github_app.command("push")
def github_push_cmd(
commit_message: str = typer.Option(
"mindXtrain initial push", "--message", "-m", help="commit message"
),
force: bool = typer.Option(False, "--force", help="use --force-with-lease on push"),
) -> None:
"""Bootstrap a git repo, create the GitHub remote (via `gh`), push the working tree.
Requires GITHUB_TOKEN + GITHUB_REPO in the environment. Reuses the same
builders as the Coach UI's "Push to GitHub" button — output is local-shell
rather than SSE-streamed.
"""
import os
import subprocess
from mindxtrain.deploy.github_push import GithubConfig, bootstrap_steps, status_missing
missing = status_missing()
if missing:
console.print(f"[red]missing:[/red] {', '.join(missing)}")
console.print("[yellow]hint:[/yellow] set GITHUB_TOKEN and GITHUB_REPO, install gh + git")
raise typer.Exit(code=2)
cfg = GithubConfig(
token=os.environ["GITHUB_TOKEN"],
repo=os.environ["GITHUB_REPO"],
branch=os.environ.get("GITHUB_DEFAULT_BRANCH", "main"),
author_name=os.environ.get("GITHUB_AUTHOR_NAME", "mindXtrain bot"),
author_email=os.environ.get("GITHUB_AUTHOR_EMAIL", "noreply@pythai.net"),
)
rcs: dict[str, int] = {}
for step in bootstrap_steps(cfg, commit_message=commit_message, force=force):
if step.predicate_step is not None:
gate = rcs.get(step.predicate_step)
if gate is None or gate not in step.predicate_rc_in:
console.print(f"[dim]skip[/dim] {step.label}")
rcs[step.label] = -1
continue
console.print(f"[cyan]→ {step.label}[/cyan]: {' '.join(step.cmd[:6])}…")
proc = subprocess.run(step.cmd, env=step.env or None, check=False)
rcs[step.label] = proc.returncode
if proc.returncode != 0 and not step.allow_failure:
console.print(f"[red]{step.label} failed (rc={proc.returncode}); aborting[/red]")
raise typer.Exit(code=3)
console.print("[green]push complete[/green]")
@droplet_app.command("provision")
def droplet_provision_cmd(
name: str = typer.Option("mindxtrain", "--name"),
repo: str = typer.Option(None, "--repo", help="defaults to $GITHUB_REPO"),
branch: str = typer.Option(None, "--branch", help="defaults to $GITHUB_DEFAULT_BRANCH or 'main'"),
container: str = typer.Option(None, "--container", help="defaults to $DROPLET_CONTAINER"),
extras: str = typer.Option("ml,eval,data,obs", "--extras"),
wait: bool = typer.Option(True, "--wait/--no-wait", help="poll for cloud-init bootstrap completion"),
) -> None:
"""POST a new MI300X droplet to AMD Dev Cloud + wait for cloud-init bootstrap.
Requires AMD_DEV_CLOUD_TOKEN + AMD_DEV_CLOUD_SSH_KEY_ID. The droplet's
`user_data` clones from GitHub and runs `mindxtrain bench` as it boots, so
by the time SSH is reachable the autotune plan is on disk.
"""
import os
import time
from mindxtrain.deploy import amd_dev_cloud as adc
from mindxtrain.deploy.cloud_init import render
missing = adc.missing_env()
if missing:
console.print(f"[red]missing:[/red] {', '.join(missing)}")
raise typer.Exit(code=2)
cloud_cfg = adc.from_env()
user_data = render(
repo=repo or os.environ.get("GITHUB_REPO", "professor-codephreak/mindXtrain"),
branch=branch or os.environ.get("GITHUB_DEFAULT_BRANCH", "main"),
container=container or os.environ.get("DROPLET_CONTAINER", "rocm/primus:v26.2"),
extras=extras,
)
log = console.print
with adc.AmdDevCloudClient(cloud_cfg) as client:
droplet = client.create(name=name, user_data=user_data, log=lambda line: log(f"[cyan]{line}[/cyan]"))
droplet_id = int(droplet["id"])
if not wait:
console.print(f"[green]droplet_id={droplet_id}[/green] — exiting before bootstrap (--no-wait)")
return
droplet = client.poll_until_active(
droplet_id, log=lambda line: log(f"[dim]{line}[/dim]"), sleep=time.sleep, now=time.monotonic
)
ip = adc.extract_public_ip(droplet) or ""
console.print(f"[green]droplet_id={droplet_id} public_ip={ip}[/green]")
@droplet_app.command("sync")
def droplet_sync_cmd(
no_bench: bool = typer.Option(False, "--no-bench", help="rsync + provision only, skip bench"),
no_fetch: bool = typer.Option(False, "--no-fetch", help="don't scp plan.json back"),
) -> None:
"""Rsync the working tree to $DROPLET_HOST + run bench inside rocm/primus.
Requires DROPLET_HOST + DROPLET_USER. Reuses the same builders as the
Coach UI's "Sync to existing droplet" button — output is local-shell.
"""
import subprocess
from mindxtrain.deploy.droplet import from_env, status_missing, sync_steps
missing = status_missing()
if missing:
console.print(f"[red]missing:[/red] {', '.join(missing)}")
raise typer.Exit(code=2)
cfg = from_env()
plan_dest = Path("./out/plan.remote.json")
plan_dest.parent.mkdir(parents=True, exist_ok=True)
for step in sync_steps(
cfg,
repo_root=Path.cwd(),
run_bench=not no_bench,
fetch_plan=not no_fetch,
plan_dest=plan_dest,
):
console.print(f"[cyan]→ {step.label}[/cyan]")
proc = subprocess.run(step.cmd, env=step.env or None, check=False)
if proc.returncode != 0 and not step.allow_failure:
console.print(f"[red]{step.label} failed (rc={proc.returncode})[/red]")
raise typer.Exit(code=3)
console.print("[green]sync complete[/green]")
# ---- mei verbs --------------------------------------------------------------
@mei_app.command("score")
def mei_score(
record: Path = typer.Argument(
..., help="Path to a JSON MEIRecord file (output of the measurement orchestrator).",
),
out: Path | None = typer.Option(
None, "--out", help="Optional path to write the MEIScore JSON. Defaults to stdout.",
),
append_history: bool = typer.Option(
True, "--history/--no-history",
help="Append the score to the historical-comparison ledger.",
),
) -> None:
"""Score a MEIRecord against the v0.1 anchors. Prints MEIScore JSON.
The record JSON must conform to `mindxtrain.eval.mei.record.MEIRecord`.
Generate one via the measurement orchestrator (Phase 1.4) or hand-craft
against the schema for demos.
"""
from mindxtrain.eval.mei.history import append as _hist_append
from mindxtrain.eval.mei.record import MEIRecord
from mindxtrain.eval.mei.score import score_record
rec = MEIRecord.model_validate_json(record.read_text())
sc = score_record(rec)
out_text = sc.model_dump_json(indent=2)
if out is not None:
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(out_text + "\n")
console.print(f"[green]wrote[/green] {out}")
else:
console.print(out_text)
if append_history:
path = _hist_append(
sc,
run_id=rec.model_id,
model_id=rec.model_id,
model_sha256=rec.model_sha256,
promoted=False,
)
console.print(f"[dim]history appended → {path}[/dim]")
# Composite headline for terminal-friendly reading.
console.print(
f"[bold]MEI[/bold] = [bold cyan]{sc.composite:.3f}[/bold cyan] "
f"Q={sc.quality:.3f} Dt={sc.decode_throughput:.3f} "
f"Pp={sc.prefill_throughput:.3f} M={sc.memory:.3f} E={sc.energy:.3f}"
+ (" [yellow](provisional Agentic)[/yellow]" if sc.mab_provisional else ""),
)
# Promotion preview (against the current ledger).
from mindxtrain.eval.mei.history import currently_promoted
from mindxtrain.eval.mei.score import is_promotable
prior = currently_promoted()
prior_score = prior.score if prior is not None else None
ok, reasons = is_promotable(sc, prior_promoted=prior_score)
if ok:
console.print("[green]✓ promotable[/green] — eligible for AgenticPlace.")
else:
console.print("[yellow]✗ not promotable[/yellow]:")
for r in reasons:
console.print(f" • {r}")
@mei_app.command("history")
def mei_history(
last: int = typer.Option(10, "--last", "-n", help="Show the last N entries."),
promoted_only: bool = typer.Option(
False, "--promoted-only", help="Filter to entries promoted to AgenticPlace.",
),
) -> None:
"""List recent MEI scores from the historical ledger."""
from mindxtrain.eval.mei.history import read_all
rows = read_all()
if promoted_only:
rows = [r for r in rows if r.promoted]
rows = rows[-last:] if last > 0 else rows
if not rows:
console.print("[dim](no MEI history yet — run `mindxtrain mei score …`)[/dim]")
return
for r in rows:
mark = "[green]★[/green]" if r.promoted else "·"
flag = " [yellow](prov)[/yellow]" if r.score.mab_provisional else ""
console.print(
f"{mark} {r.timestamp} {r.model_id} "
f"MEI={r.score.composite:.3f}{flag}",
)
if __name__ == "__main__":
app()