fix(train): apr train apply trained on fabricated data and reported success - #2436
fix(train): apr train apply trained on fabricated data and reported success#2436noahgift wants to merge 2 commits into
Conversation
…uccess
`apr train apply --task pretrain --config c.yaml` on a 200-row CSV printed one
line to stderr — `Warning: Unsupported data format 'csv', using demo data` — then
ran two epochs at a constant loss of 0.010000, wrote a 21 MB checkpoint, printed
`DONE Pre-training completed` and exited 0. The 0.010000 is the synthetic fixture
value; the user's corpus was never opened. A JSONL dataset and a well-formed JSON
array of records produced the byte-identical fabricated loss. In CI that is a
green training job for a model that saw none of your data.
The loader had three demo-data fallbacks and its own doc comment was wrong:
crates/aprender-train/src/config/train/batches/loader.rs:15 claimed "Supports
parquet, JSON, and CSV formats via alimentar" while the match handled only
parquet and json and fell through to `create_demo_batches` for everything else.
json.rs did the same on a parse failure, parquet.rs on unrecognised columns.
All four now return `Error::ConfigError` naming the dataset and quoting the
schema the loader actually wants, and `create_demo_batches` is deleted outright
so nothing can reach for it again.
Four tests asserted `is_ok()` on inputs that should fail — they encoded the
defect and would have blocked this fix. They are rewritten to assert the error.
Two more P0s in the same family:
`apr train apply --task pretrain` aborted with exit 101 on any tabular dataset
whose input width differed from its target width — a 3-feature / 1-target
regression set, the commonest tabular shape there is. Sweeping six (input,
target) pairs, it survived only when input_dim == target_dim: (1,1) and (2,2)
and (4,4) trained; (2,1), (3,1) and (1,2) hit a raw `assert_eq!` in
`MSELoss::forward` after "Starting training..." had printed. Tabular mode drives
the generic Trainer with an identity forward, so the widths must match;
`validate_tabular_batch_shapes` now says so before any training runs.
`apr finetune <model>.apr --task classify` discarded the file the user named and
handed its PARENT DIRECTORY to `ClassifyPipeline::from_pretrained`, which scans
for any SafeTensors it can find (finetune.rs:2183). Dropping one unrelated 4.6 MB
safetensors next to a 0.5B .apr, changing nothing else, flipped the run from "No
SafeTensors files found" to loading the sibling's 27 tensors. The stale comment
said `from_apr()` was unavailable in entrenar 0.7.5; entrenar is in-tree at
0.63.0 and `Transformer::from_apr` exists. Had the sibling's dims matched it
would have fine-tuned the wrong weights and exited 0.
Also in this cluster:
- `apr train apply -o DIR` was documented with a default of /tmp/training-output
and silently discarded; only training.output_dir in the YAML was honoured, and
its destination was never created, so a completed run was thrown away at the
save step with a bare "No such file or directory (os error 2)". -o now
overrides the YAML and the directory is created before saving.
- `apr tune --rank R` echoed "Requested rank: R" and reported recommended_rank
256 for every R in {4, 8, 16, 64, 256, 1024}: the recommendation was a pure
function of --vram. `plan_with_rank` pins the rank and derives alpha,
trainable params, memory and the rank-aware LR from it.
- `apr train plan --format` was declared `_format: &str` and never read; text,
json, yaml and an invalid value produced byte-identical text with exit 0.
yaml now renders the manifest; an unknown value is rejected.
- `apr runs ls --status completed` matched 0 of 2941 completed runs because the
filter stringified the stored variant (`Success`) and compared it verbatim,
and `--status bogusvalue` returned an empty table with exit 0.
- `apr tune --method bogus` and `apr train sweep --strategy bogus` fell through
to Auto and to a RANDOM search respectively, printing the typo back as though
it were valid. Both now reject, matching finetune/distill/prune.
- `apr pretrain` printed "OK CONVERGED" without ever comparing final_val_loss
against the target: final 3.0000 against a target of 0.001 reported CONVERGED
with exit 0, and the JSON report carried neither the target nor a verdict.
- `apr finetune <model>.safetensors --task classify` replied "No model path or
--model-size provided" when the path was the first positional argument.
- `apr grad-norm` reported a malformed JSON telemetry file as "Invalid APR
format"; it never touches a model. New `CliError::InvalidInput`, same exit 4.
Mutation-verified: with every fix reverted and every test kept, the new
falsifiers turn RED, then GREEN on restore.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ath appears Under the old parent-directory scan the .safetensors path appeared in the error too — it was the file that got scanned — so a contains-the-path assertion alone stayed GREEN with the defect restored. Caught by the mutation check, not by review. Refs #2374 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mutation check — verbatim REDAll 17 fix sites reverted, every test kept.
The mutation caught a weak testMy first pass mutated finding 1 as That same run showed |
|
Temporarily closing to stop CI contention — the branch is untouched and this will be reopened, nothing is lost. The shared Reopening in batches as the merge queue drains. The work is complete and reviewed; only the CI scheduling is being paced. |
|
Superseded by #2449 — this branch is merged verbatim into that batch. The binding constraint was one ~50-minute Closing rather than leaving open so this PR cannot move #2449's base and force it to re-run. The branch is untouched and this is reopenable if the batch does not land. |
apr train apply --task pretrain --config c.yamlon a 200-row CSV printed one line to stderr —Warning: Unsupported data format 'csv', using demo data— then ran two epochs at a constant loss of0.010000, wrote a 21 MB checkpoint, printedDONE Pre-training completedand exited 0. The0.010000is the synthetic fixture value; the user's corpus was never opened. A JSONL dataset and a well-formed JSON array of records produced the byte-identical fabricated loss. In CI that is a green training job for a model that saw none of your data.This PR fixes 11 of the 15 findings in #2374, worst first.
Before / after, measured
Run against a release
aprbuilt from this branch (/mnt/nvme-raid0/coverage/aprender/release/apr). Note the first attempt at this table used$(cargo metadata …).target_directory, which pointed at a different directory than the build actually wrote — that binary was another agent's and reported the old behaviour for every finding. The numbers below are from the binary whose mtime matches the build.rc=0, loss0.010000, 21 MB checkpointrc=5Unsupported training data format 'csv' for 'tab.csv'. Supported: json. Convert the dataset to JSON — expected JSON of the form {"examples":[…]}rc=0, same fabricated lossrc=5Could not parse training data 'bad.json': expected JSON of the form …rc=101, rawassert_eq!inMSELoss::forwardrc=5batch 0 has 24 input values but 8 target values — input width 3 vs target width 1 …Final loss: 0.153731(unchanged)-o DIRignored; checkpoint went to./checkpointsSaving model to /tmp/…/FLAG_OUT/final_model.json, directory createda63600bb, allrc=0a63600bb/b717d476/2071f6d6;bogus→rc=5Unknown --format 'bogus'. Supported: text, json, yamlrecommended_rank = 256--status completed→ 0 of 2948;bogusvalue→ 0 rowsrc=0completed→ 2948;bogusvalue→rc=5Unknown run status …tune --method bogus→rc=0planned Auto;sweep --strategy bogus→rc=0, wrote a random sweeprc=5; no sweep directory createderror: Invalid APR format: apr grad-norm: failed to parse JSON …error: Invalid input: apr grad-norm: failed to parse JSON …(still exit 4)No model path or --model-size provided(the path was the first positional arg)Could not read the model architecture from '…064a3693fa1ea02c.safetensors' (format: safetensors). Pass --model-size to state it explicitly.apr+ one unrelated sibling →Failed to load APR model: No SafeTensors files found in <dir>Failed to load APR model 'm.apr': …— the named file, sibling never openedOK CONVERGED final val_loss=3.0000at target0.001OK NOT_CONVERGED final val_loss=3.0000 > target 0.0010;--jsonnow carriestarget_val_lossandconverged: falseRoot causes
crates/aprender-train/src/config/train/batches/loader.rs:15documented "Supports parquet, JSON, and CSV formats via alimentar" while the match at 32-46 handled onlyparquet/jsonand fell through tocreate_demo_batches.json.rs:64did the same on a parse failure;parquet.rson unrecognised columns. All four now returnError::ConfigError, andcreate_demo_batchesis deleted so nothing can reach for it again.Trainerwith an identity forward, soMSELossneeds equal widths. Sweeping six (input, target) pairs, it survived only wheninput_dim == target_dim. Newvalidate_tabular_batch_shapesrejects before any training runs.crates/apr-cli/src/commands/finetune.rs:2183discarded the file and passed its parent directory toClassifyPipeline::from_pretrained, which scans for any SafeTensors. The comment claimedfrom_apr()was unavailable in entrenar 0.7.5; entrenar is in-tree at 0.63.0 andTransformer::from_aprexists (aprender-train/src/transformer/model.rs:153).-oflag was never read; onlytraining.output_dirwas, and its destination was never created.apr tunecalledplan(), which auto-selects rank purely from--vram. Newplan_with_rankpins it and derives alpha, trainable params, memory and the rank-aware LR from it._format: &str, never read. 13 —unwrap_or(TuneMethod::Auto)swallowed a perfectly goodFromStrerror;"random" | _swallowed the strategy. 8 — the filter stringified the stored variant (Success) and compared it verbatim. 11 —RunStatus::Okmapped to the literal"CONVERGED"without readingtarget_val_loss. 15 —CliError::InvalidFormat's Display is hardcoded to "Invalid APR format".Tests that encoded the defect
Nine tests asserted
is_ok()on inputs that should fail; they would have blocked this fix and are rewritten to assert the error:test_load_training_batches_nonexistent_file,test_load_training_batches_unsupported_format,test_load_json_batches_invalid_format(×2 modules),test_load_training_batches_missing_file,test_load_training_batches_unsupported_extension,test_handle_missing_columns_returns_demo_batchestest_train_from_yaml_success/_with_grad_clip/_with_lora/_with_quantize/_default_mode_is_tabularpointeddata.trainat atrain.parquetthat did not exist and asserted the whole run succeeded and wrotefinal_model.json— the fabricated-data path existed to make them pass. They now supply the dataset they always claimed to be training on.run_plan_*tests passed"apr"as--format; that only worked because the flag was dead. They now pass"text".Mutation check
With all 17 fix sites reverted and every test kept (
scratch_mutate.pyin the branch history), the new falsifiers turn RED; restoring turns them GREEN. Verbatim RED output is in the PR thread.Gates
cargo fmt --all -- --checkcleancargo test -p apr-cli --lib— 6690 passed, 0 failedcargo test -p aprender-train --lib— the only remaining failures areprune::snapshot_tests::{all_prune_methods, pipeline_stages, schedule_validation_errors}, which reproduce identically on a clean tree at HEAD (8cc3aaf) with this branch stashed: 14 passed / 3 failed both ways. Pre-existing, JSON key-ordering, in a module this PR does not touch.Not fixed (left open)
train plan/apply --task classifyis a stub whose message claims entrenar >= 0.8 is unpublished (it is in-tree at 0.63.0). The honest fix is to implement it or delete the surface; both are larger than this PR.prune --plansize estimate.train halvingignores trial exit status.pretrain --num-stepsrounds up to a whole epoch.Also newly visible because the sibling no longer masks it: with finding 1 fixed,
apr finetune <0.5B>.apr --task classifynow fails insideTransformer::from_aprwithShape mismatch for 'lm_head.weight': expected 136134656 elements, got 0— a real, distinct defect in the APR classify loader (tied word embeddings), not a regression from this PR. Worth its own issue.Refs #2374 (partial) — remaining: 4, 9, 10, 12
Audit epic: #2373