Model training (SparkLang)

Training is a first-class language pillar. model train / model build submit jobs via backend "http" to a trainer you run at SPARK_TRAIN_URL — not a magic cloud. Dry-run fixtures demonstrate the path; live requires that service. Optional model plan is markdown only.

Eval helpers (model analyze / compare / improve) stay offline sugar. Optional plan export is model plan (markdown). Live gateway ask is optional for post-train inference checks — SparkLang is not a Bifrost plugin.

Full language forms: LANGUAGE.md. Abstain / IDK probes (not LoRA): ABSTAIN_HEADS.md.

Verbs

Statement Meaning
model train … -> job Submit a train job (dry fixtures or live backend)
model build … Same as train (rehabilitated; not blueprint)
model status ["job-id"] -> status Poll job state + artifact paths
model plan blueprint into "path" Optional markdown plan only
model analyze / compare / improve Eval / heuristic helpers (unchanged)

Minimal train form (fields optional in dry-run; fixtures fill gaps):

model train dataset "data/train.jsonl" base "base-id" out "out/train/demo" backend "http" method "spark_distill_cpu" -> job

        model status "job-dry-001" -> status
        

method "…" is optional (spark_distill_cpu default). Live GAS forwards the statement with ./spark-train-http --spark-line. Status polls the quoted job id from the line (not a hardcoded job-dry-001).

Job lifecycle

submit  →  accepted (job_id)
                →  running
                →  succeeded | failed | cancelled
        artifact paths appear on accept (planned) and again on success
        
Phase Dry-run Live
Submit Fixture accept JSON; write marker under out/train/<job_id>/ Backend adapter
Status Fixture succeeded + same paths Poll backend
GPU / net Never Only when configured

Backends (train.backend / SPARK_TRAIN_BACKEND)

Id Role
http Default MVP. POST job JSON to SPARK_TRAIN_URL; poll status
local-yield Optional host adapter: systemctl start train@<unit> when allowlisted
huggingface Reserved id — not wired in MVP (do not claim Hub publish)

Generic interface — SparkLang is not hard-wired to one machine.

HTTP (shipped companion)

# Offline (CI / make test)
        ./spark-train-http --dry --submit
        ./spark-train-http --dry --submit --method spark_pref_pack
        ./spark-train-http --dry --status job-dry-001

        # Live
        export SPARK_TRAIN_BACKEND=http
        export SPARK_TRAIN_URL=https://train.example/v1   # your API
        # optional: SPARK_TRAIN_TOKEN=…   (never commit)
        # optional: SPARK_TRAIN_METHOD=spark_distill_cpu|spark_pref_pack|spark_playbook_fit|spark_faq_index
        # optional: SPARK_TRAIN_OUT=out/train/job-…   (live out override)
        ./spark --live examples/model_train.spark
        # or:
        ./spark-train-http --live --submit \
          --method spark_pref_pack \
          --dataset data/train.jsonl --base spark_pref_pack \
          --out out/train/demo
        ./spark-train-http --live --status <job_id>
        

Trainer HTTP contract

Exact shapes ./spark-train-http sends and prints (no other endpoints):

Submit

POST {SPARK_TRAIN_URL}/jobs
        Content-Type: application/json
        Authorization: Bearer {SPARK_TRAIN_TOKEN}   # optional

        {"dataset":"…","base":"…","out":"…","backend":"http","method":"spark_distill_cpu"}
        

Response body (printed to stdout as-is):

{"job_id":"…","status":"accepted","artifacts":{…},"method":"…", …}
        

status is the submit ack (accepted). Extra fields are allowed. method selects the training algorithm (see below). If omitted, the reference trainer may also treat a matching base as the method id.

Status

GET {SPARK_TRAIN_URL}/jobs/{job_id}
        Authorization: Bearer {SPARK_TRAIN_TOKEN}   # optional
        

Response body:

{"job_id":"…","state":"succeeded|failed|running|…","artifacts":{…},"method":"…", …}
        

state is the poll field (not status). Companion uses plain HTTP (TLS must terminate upstream for https:// — MVP dies with a clear config error).

Reference methods (in-repo) — not LoRA

tools/spark-train-ref/ implements the HTTP contract with four SparkLang-native CPU methods. None are LoRA / HF PEFT. None use a voice-reserved GPU. None invent train@ grants.

Method One sentence Primary artifacts
spark_distill_cpu Tiny student learns which teacher reply class matches each user turn weights.pt (+ checkpoint.json)
spark_pref_pack Build chosen/rejected preference pairs and train a tiny ranker pref_pack.json + ranker.pt
spark_playbook_fit Fit an intent→playbook router from reply templates playbooks.json + router.pt
spark_faq_index Build FAQ corpus and train a tiny dual-encoder retriever faq_index.json + encoder.pt

Select via (first match wins):

  1. Language method "…" on model train / model build (live --spark-line)
  2. Companion --method / POST body method / env SPARK_TRAIN_METHOD
  3. Or base equal to a method id (reference trainer only)
  4. Default: spark_distill_cpu

Unknown method → fail loud (exit 2). Dry status for an unknown job id → fail loud (no silent job-dry-001).

HTTP artifacts.adapter remains a compat alias to the method’s primary weight file (not a LoRA adapter).

When out basename matches job-*, that basename is the job_id.

Live captures:

  • Distill: ../website/docs/examples/live-train-capture.txt
  • All four: ../website/docs/examples/live-train-methods-capture.txt
python3 tools/spark-train-ref/server.py --host 127.0.0.1 --port 8090
        export SPARK_TRAIN_BACKEND=http
        export SPARK_TRAIN_URL=http://127.0.0.1:8090/v1

        # method comes from the .spark line
        ./spark --live examples/model_train.spark
        ./spark --live examples/model_train_pref.spark
        ./spark --live examples/model_train_playbook.spark
        ./spark --live examples/model_train_faq.spark
        

Dry-run fixtures still plan stub paths without training.

local-yield (optional; gated)

Only when all hold:

  1. SPARK_TRAIN_BACKEND=local-yield
  2. Unit name is in SPARK_TRAIN_UNIT_ALLOWLIST (comma-separated)
  3. Operator has a real owner train-grant for that unit elsewhere — Spark does not invent train-grant tokens

Then the companion may run systemctl start train@<unit>. Training compute policy on shared hosts: coding GPU only via the yield unit; never place Spark training on a voice-only GPU.

What model build means now

Old (removed) New
Write out/*.md blueprint, train=false Submit train job / print artifacts

Blueprint markdown → model plan.

Dry proof

./spark --dry-run examples/model_train.spark
        # expect: "op":"train", job-dry-001, out/train/job-dry-001
        test -f out/train/job-dry-001/ARTIFACT
        ./spark-train-http --dry --submit | grep job-dry-001
        ./spark-train-http --dry --submit --method spark_pref_pack | grep spark_pref_pack
        ./spark-train-http --dry --submit --method spark_faq_index | grep spark_faq_index
        

make test never starts GPU jobs or dials the network.

Product story

Spark ships multiple CPU training methods behind one HTTP contract — distill, preference pack, playbook fit, FAQ index. That is the product story: not LoRA-by-default, not a marker file pretending to be weights. Larger full-SFT / multi-node remain operator backends behind the same contract.

Not in MVP

  • Auto dataset curation / labeling UI
  • Hugging Face Hub publish
  • Full LoRA studio / multi-node scheduler UI
  • Invented owner train-grant strings
  • Bifrost alias pickers as “Step 2” of building a model
  • Language-level method "…" keyword on model train (use env / companion)