Mova0.5 pre-release⌕
☰
home / docs / train / Word problems: exact core, step solver, world scripts, world reader, SVAMP / GSM8K

Word problems: exact core, step solver, world scripts, world reader, SVAMP / GSM8K

Crates: math (exact core, step solver mathsolve, world reader qread) and world (world scripts: format, checker, optional LLM writer). Both belong to the root Cargo workspace; pick one with -p or --manifest-path. math has three binaries (math, mathsolve, vmmtests) and no default-run, so --bin is required:

cargo run --release --manifest-path math/Cargo.toml --bin mathsolve -- <cmd> ...
cargo run --release --manifest-path math/Cargo.toml --bin math      -- <cmd> ...
cargo run --release --manifest-path world/Cargo.toml                -- <cmd> ...

0. Setup (read once)

Gist

Three things to know before running anything beyond calc and controls.

Steps

  1. Parser model. mathsolve (steps, qread, qread-text) loads the en UD model shipped in en/models/en.bin (the path is fixed at build time). To use your own model, train it with en train (see retrain-the-parser.md) and replace that file, or use MATH_EN_MODEL with the math binary.
  2. Run directory. Every mathsolve command creates its run directory at startup: MATH2_RUN, or math2-2026-09-26. Set it explicitly:
    export MATH2_RUN=runs/math2
    
  3. Dataset root. mathsolve steps|qread and world quant-* read datasets from raw (MOVA_DATA defaults to data in the current folder). Call that directory <DATA_ROOT> below. The expected files are listed in section 6. The world scripts come with the corpus: train/get-corpus.sh puts them in corpus/math-scripts/.

Pitfalls


1. Exact core

Gist

All arithmetic is exact:

calc.rs evaluates expressions on v2. Every result gets an independent check: the same expression computed with different arithmetic (f64, or a fingerprint modulo 2^61−1 for huge integers). A failed check blocks the answer.

When to use

Steps

# expression -> exact value + independent check
cargo run --release --manifest-path math/Cargo.toml --bin mathsolve -- calc "3^40 + 17" "(16 - 3 - 4) * 2"

# negative controls: each must be RED (caught); exits non-zero if any passes
cargo run --release --manifest-path math/Cargo.toml --bin mathsolve -- controls

# English question -> expression tree -> steps, checks, answer (units, %, stats, probability)
cargo run --release --manifest-path math/Cargo.toml --bin math -- eval "What is 15% of 80?"

# frozen suite: id <TAB> question <TAB> expected
cargo run --release --manifest-path math/Cargo.toml --bin math -- test math/data/suite-v1.tsv

Example

expr:    3^40+17
  ✓ independent path: fingerprint modulo 2^61−1 fingerprint 628450412988459063
answer: 12157665459056928818

controls ends with negative controls red: 7 of 7.

In math test, the "expected" column takes one of:

The summary line # TOTAL <n> <hit> <honest NU> <wrong> gives the totals.

Pitfalls


2. Step solver: mathsolve steps

Gist

A transition system reads the numbers in text order and, at each one, chooses SHIFT, SKIP or REDUCE (op + direction). The features come from the en UD tree (unit noun, verb class, subject, cue words, question words). The model is a structured perceptron with beam search and early update. The exact core computes every subtree.

With --exact, the gold trees come from SVAMP equations (( 290.0 / 2.0 )) or GSM8K calculator steps (<<48/2=24>>). Without it, the gold trees are guessed by weak supervision. Training happens in memory on every run; no model file is written.

When to use

Steps

The flags come from the "steps" arm of math/src/bin/mathsolve.rs. Defaults are in brackets.

flagmeaning
--set gsm8k|svamp|mix|tiny [gsm8k]svamp: train on SVAMP train, test on SVAMP test. gsm8k: train on GSM8K socratic train, test on its test split. mix: train on SVAMP + GSM8K, test on SVAMP. tiny: train on GSM8K + a TinyGSM sample (--tiny N [20000]), test on GSM8K
--exactexact gold trees from the annotation (recommended; needed by --world, --princ, --qread, --arith-feat)
--train N [all]cap on training items
--epochs E [5] / --beam B [16] / --maxq Q [9]perceptron epochs, beam width, max quantities per problem
--rerank K [0]train a judge over the top-K candidates. Its training candidates come from models trained without their own fold (4 folds)
--world <scripts.jsonl>train the world writer on verified world scripts (section 4)
--world-featuse the learned world's answer as a judge feature (with --world)
--qreaduse the world reader's answer as a judge feature. It only counts when every number was read (section 5)
--princ <scripts.jsonl>sentence-principle classifier from scripts (5-fold cross-check printed)
--dagv5 DAG model (trees with repeated numbers; text order not needed)
--augment, --curriculum, --star N, --shapes, --arith-feat, --bag, --bag-judge, --rethinkexperimental variants (data augmentation, curriculum, self-training rounds, shape library, ensembles, second pass with a wider beam)
--show N, --explain Nprint the first N test items with their steps / a full explanation
--hints Ncalls an LLM via a CLI for up to N disagreement cases, with a small spend cap. Optional; leave it off for local runs

Environment variables:

Example

The configuration used as the benchmark baseline:

export MATH2_RUN=runs/math2
MATH_NOLEX=1 cargo run --release --manifest-path math/Cargo.toml --bin mathsolve -- \
  steps --set svamp --exact --epochs 20 --beam 32 --rerank 5 --world corpus/math-scripts/scripts.jsonl

# GSM8K, same settings without --world
MATH_NOLEX=1 cargo run --release --manifest-path math/Cargo.toml --bin mathsolve -- \
  steps --set gsm8k --exact --epochs 20 --beam 32 --rerank 5

Add --qread to the SVAMP command to use the world reader as a judge feature.

What the output reports (in order):

  1. <set> (exact gold): training with gold N; no tree …; no exact tree X; tree not in text order Y. This is the training data and the excluded items with their reasons: no exact tree, or a tree not buildable in text order.
  2. epoch k: correct on training a/N: training accuracy per epoch.
  3. <set> test: SLM alone (v4) correct acc/n (p%): the headline test accuracy.
  4. top-K: correct among them …: how often the right answer is among the top-K candidates.
  5. Self-assessment:
    • accuracy when only answering above a confidence threshold (threshold);
    • accuracy when the first model and the judge agree (opinions agree);
    • accuracy for the most confident 10/20/30/50% (most confident);
    • trigger counts, e.g. v2: answered — correct / wrong and world: … (world agreement).

Pitfalls


3. World scripts: format

Gist

A world script turns a word problem into a quantity world:

The executor (world/src/quant.rs: parse_line, run, check) runs scripts with the exact core. It accepts a script only if it passes every gate.

When to use

Steps

The line grammar comes from parse_line. Blank lines and lines starting with # are skipped.

@N set   E.x = EXPR          k=KIND
@N add   E.x = EXPR          k=KIND
@N sub   E.x = EXPR          k=KIND
@N mul   E.x = EXPR          k=KIND
@N div   E.x = EXPR          k=KIND
@N move  A.x -> B.x = EXPR   k=KIND      (from A to B; A must exist)
@N ask   EXPR                            (k= optional)

Gates (each reports an error):

Corpus file (corpus/math-scripts/, JSONL, one problem per line; written by quant-vmm, read by load_done, quant-stats and steps --world/--princ):

{"id":"svamp-train-chal-777","set":"svamp-train","question":"There are 87 oranges ... How big is each group of bananas?",
 "gold":"145","script":"@1 set philip.orange = 87 k=given\n...\n@3 ask philip.banana_per_group\n",
 "ok":true,"errors":[],"round":1}

Example

Script file s.txt for SVAMP train item chal-777:

@1 set philip.orange = 87 k=given
@1 set philip.banana = 290 k=given
@2 set philip.banana_group = 2 k=given
@3 set philip.banana_per_group = philip.banana / philip.banana_group k=unit
@3 ask philip.banana_per_group
cargo run --release --manifest-path world/Cargo.toml -- quant-check svamp-train svamp-train-chal-777 s.txt
# answer Some("145"); errors 0
cargo run --release --manifest-path world/Cargo.toml -- quant-stats corpus/math-scripts
# green counts per set, plus principle and verb histograms (reads <dir>/scripts.jsonl)

A script that uses 300 instead of 290 fails with two errors: number 300 not from the text up to sentence @1 and answer 300 ≠ reference 145.

Pitfalls


4. Training the world writer

Gist

The small model learns to write the quantity world itself (math/src/qworld.rs):

Labels come from verified world scripts (section 3). Without trained models, a rule-based writer (qworld::write) is used.

When to use

You have verified scripts (shipped in corpus/math-scripts/ or generated yourself) and want the world as a third opinion or as a judge feature.

Steps

  1. Optional: generate more scripts with an LLM. quant-vmm sends batches to an LLM through a CLI (claude -p), checks each script, retries red ones once with the error list (the gold answer is never shown), and appends to <dir>/scripts.jsonl. LLM costs are logged in <dir>/calls.jsonl. It resumes by skipping ids already present. This step is paid and optional:
    MOVA_SUBSCRIPTION=1 cargo run --release --manifest-path world/Cargo.toml -- \
      quant-vmm svamp-train runs/quant --limit 20 --batch 10 --par 2 --cap 2
    
    MOVA_SUBSCRIPTION=1 makes the CLI use its own login instead of looking for a locally stored API key. --cap is the spend ceiling in USD [10]. The model and effort come from PRAG_MODEL / PRAG_EFFORT [medium].
  2. Train and use the writer inside steps (it is not saved; it is retrained on every run):
    cargo run --release --manifest-path math/Cargo.toml --bin mathsolve -- \
      steps --set svamp --exact --rerank 5 --world corpus/math-scripts/scripts.jsonl --world-feat
    
  3. Read the report lines:
    • world: training problems with scripts N, number labels M: training problems matched to scripts, and labelled numbers;
    • query: templates from scripts …: template histogram;
    • at test time, world: correct, agrees with judge and similar lines count how often the world was right and whether it agreed with the judge.

Pitfalls


5. World reader: qread (world v2 for numbers)

Gist

math/src/qread.rs reads a problem as events in time:

"He had 492 left" or "if he has 116 now" are not events but conditions on the state. The world solves for the unknowns from them (linear equations). The question becomes a query: state now / initially, sum of a class of events, difference, total.

If the world doesn't understand something, it returns None with a reason in the log and lists the numbers it could not read (unread). It does not guess. Rates and shares ("each", "per", "half", …) are left to steps.

When to use

Steps

# probe one problem (the last sentence is the question)
cargo run --release --manifest-path math/Cargo.toml --bin mathsolve -- qread-text \
  "Paul had some crayons. He gave 52 crayons to his friends and lost 535 crayons. He had 492 crayons left. How many crayons did he have at first?"

# coverage and accuracy on SVAMP: qread [svamp|svamp-train] [N = wrong answers to print]
cargo run --release --manifest-path math/Cargo.toml --bin mathsolve -- qread svamp 5

Example

Some(1079.0) unread []
t0: paul have(have) crayon Set None
t1: paul give(give) crayon Sub Some(52.0)
t1: paul lose(lose) crayon Sub Some(535.0)
t2: paul have(have) crayon Is Some(492.0)
state initially Some("paul") Some(("crayon", "crayon"))

unread lists the numbers the world could not read; state initially is the state at the start.

qread svamp prints svamp: the world answered A/300, of them correct K (p%): the world answered A of 300, and K of those were right. Report both coverage (A/300) and accuracy (K/A).

Pitfalls


6. Evaluating on SVAMP / GSM8K

Gist

The datasets are not in the repo. Download them (both MIT) and convert them to the JSON arrays the loaders expect under <DATA_ROOT> (section 0).

Steps

  1. SVAMP: take the 700/300 split from Hugging Face ChilleD/SVAMP (train.json, test.json). The original 1000 problems are on GitHub at arkilpatel/SVAMP (same fields, no split). Each item needs ID, Body, Question, Equation, Answer.
    <DATA_ROOT>/mwpdata-svamp/svamp-train.json
    <DATA_ROOT>/mwpdata-svamp/svamp-test.json
    
  2. GSM8K: take the socratic files from GitHub openai/grade_school_math (grade_school_math/data/train_socratic.jsonl, test_socratic.jsonl). The loaders want a JSON array, not JSONL. Fields: question, and answer with <<…>> steps and a final #### N.
    jq -s . train_socratic.jsonl > <DATA_ROOT>/mwpdata-gsm8k-socratic/gsm8k-socratic-train.json
    jq -s . test_socratic.jsonl  > <DATA_ROOT>/mwpdata-gsm8k-socratic/gsm8k-socratic-test.json
    
  3. Run the commands from section 2 (steps --set svamp|gsm8k …) and section 5 (qread svamp). Each command prints its own summary; see the output lists above.
  4. Before claiming any change:
    • run twice and confirm identical results (diff MATH_DUMP);
    • report the excluded training items (line 1 of steps) and the reader's coverage;
    • compare per item against the baseline.

Example

SVAMP test (300 problems) with the benchmark configuration plus --qread:

GSM8K test (1319) was around 10–11% with the same solver. Treat these figures as a snapshot, not a guarantee.

Pitfalls

This page in Markdown → https://md.mova.elamur.ai/free/en/docs/train/math-world/