Fix CodeGen inference, make it fast, make training train - #15
Conversation
Two passes over the CodeGen path. The first fixed correctness; the second made it fast and brought the documentation back in line with the code. Correctness — the model could not produce valid output before this: - The fused qkv_proj tensor was split as [all q | all v | all k]. CodeGen stores it as four interleaved model-parallel groups of [q | v | k] (mp_num = 4 upstream), so every attention head was reading the wrong slice of the projection. - Sampling was always greedy: the hand-rolled LCG divided a u64 state by u32::MAX, producing values around 6e9, so the selection loop always fell through to argmax and temperature, top_k and top_p did nothing. - complete, repl and the server all stripped prompt.len() tokens off the front of a generated-only token vector, dropping output or panicking. - The HTTP server loaded with CodeGenConfig::default(), ignoring both config.json (rotary_dim 64 rather than the checkpoint's 32) and --f16. - Missing checkpoint tensors were silently ignored, leaving zeroed layers. - CodeGenModel::new_blank hardcoded F32 regardless of config.dtype, so an F16 model panicked on its first forward. Performance, measured on the benchmark model: - KV cache: slice_assign zero-padded the source to the full buffer shape, built a full-size mask and ran where_cond over every element, for k and v, per layer, per token. slice_set copies only the new tokens in place. Decode 58.4 ms -> 5.6 ms per token; prefill 88.5 ms -> 36.2 ms. - lm_head no longer runs over the whole prompt when only the final position is read: 6.0 ms of a 35.8 ms prefill. - Embedding uses one index_select; per-token stderr timing removed. Verification and honesty: - tests/codegen_parity.rs checks logits against the HuggingFace reference using committed tiny-model fixtures, so no weight download is needed. - benches/transformer.rs now calls this crate. Every benchmark used to be a hand-inlined copy of the model, carrying the QKV bug fixed above. - main.rs uses the library instead of re-declaring the module tree, which had the binary compiling a second copy and reporting 34 dead-code warnings. cargo clippy --all-targets --all-features -- -D warnings goes from exit 101 to exit 0. - Deleted src/codegen/quantized.rs: never constructed, and it dequantized the whole weight matrix per call, making it slower than the F32 path. README, docs and the comparison table no longer claim it. - Added safetensors loading, so the converter's output is loadable. 89 tests pass.
Both cross-entropy paths summed into an f32 and returned Tensor::new(total, device) — a fresh leaf with no autograd history. Backprop from a constant yields an empty gradient store, so AdamW updated nothing and every training step was a no-op. The Var wiring was fine; only the loss was detached. Rebuilt around candle_nn::loss::cross_entropy over the labelled positions, and deduplicated into src/training/loss.rs. A real 80-step run now falls from loss 10.80 to 4.99. Before the fix, 60 optimizer steps left the loss at 4.1934047 both before and after, and 0 of 19 parameters received gradients. Also on the training path, all found while verifying the above: - Every checkpoint save failed. save_safetensors called to_vec1::<u8>(), which errors on rank > 1, so a run ended in "unexpected rank, expected: 1, got: 2" the moment it reached a weight matrix. - configs/train.yaml did not deserialize: eval_steps sat under a separate evaluation: section and tokenizer_path was missing. Nothing had loaded the file because glm-train had no --config flag. Added the flag, and #[serde(default)] so partial configs fall back field by field. - gradient_accumulation_steps and max_grad_norm were declared in the config and claimed as delivered, but read nowhere. Both are implemented now. - The learning rate was applied after optimizer.step, so each step ran on the previous step's rate. - Masking re-seeded an LCG from the step counter each step, making the corruption pattern nearly identical between steps. Uses one StdRng seeded from TrainingConfig::seed. - Deleted src/training/checkpoint.rs: 192 lines never declared in mod.rs, so never compiled, duplicating the checkpoint code in train.rs. Tests that would have caught this: gradients must reach parameters, a tiny model must memorise a batch, an optimizer step must change weights, clipping must bound the global norm, and configs/train.yaml must parse. 98 tests pass.
GitMind PR Review{'pr_url': '#15', 'repo': 'Ayyankhan101/Transformer-In-Rust', 'pr_num': '15', 'title': 'Fix CodeGen inference, make it fast, make training train', 'author': 'Ayyankhan101', 'additions': 1701, 'deletions': 1509, 'changed_files': 48, 'review': '## Summary\nAutomatic review could not be generated — please review the diff manually.\n\n## Issues\nNo significant issues found.\n\n## Verdict\n💬 NEEDS DISCUSSION\nThe automated review failed; human review required.'} Auto-generated by GitMind — AI-powered code analysis. |
First run of this code against Salesforce/codegen-350M-multi. The inference fixes hold — greedy decoding produces correct recursive fibonacci — but four things on the path to that run were broken, all invisible without weights. - codegen download reported success without downloading anything. huggingface-cli has been renamed to hf; the old name prints a deprecation notice, downloads nothing, and exits 0. The command believed the exit code and printed "Download complete!" over an empty directory. It now tries hf first and checks that the file exists rather than trusting the status. - complete printed nothing in its default streaming mode. CodeGenGenerator::with_tokenizer existed but was never called anywhere, so decode_token returned an empty string for every token. - chat sliced off the wrong tokens — the same generated-only slicing bug fixed in complete, repl and the server previously, missed in this fourth caller. - -t was bound to both --temperature and --template, and --stream was a flag with default_value = "true", so it could never be false and the non-streaming branch was unreachable. --template loses its short flag; --stream becomes --no-stream. - info printed hardcoded model values that had drifted from the checkpoint (max_seq_len 1024 against the real 2048) while parsing the real config a few lines below, and only looked for pytorch_model.bin. Measured on Apple M1 Pro, 64 tokens, greedy: F32 68.8 ms/token 4.4 s 1.63 GB peak F16 20.3 ms/token 1.3 s 1.08 GB peak F16 is 3.4x faster, not the 23% the README claimed. README and docs now carry these numbers instead of unverified ones. CodeGen pads vocab_size to 51200 while the tokenizer stops at 50294. Those 905 untrained rows were expected to be a sampling hazard, so generation was going to mask them — but they never win, verified at temperature 2.0 with no top-k or nucleus cut. The masking code was removed and the test kept, so a regression would surface rather than being silently suppressed. Also verified: the safetensors converter's output loads and produces byte- identical greedy output to the pickle path, and ModelContext picks it up automatically. 102 tests pass, including four that finally run against real weights.
Validated against the real 350M checkpointDownloaded def fibonacci(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fibonacci(n-1) + fibonacci(n-2)That confirms the QKV model-parallel split against Salesforce's actual weights, not just the parity fixtures. Both integration tests that had always self-skipped now run, plus two new ones. Four more bugs, all invisible without weights
Real numbersApple M1 Pro, 64 tokens from
F16 is 3.4× faster, not the 23% the README claimed. README and docs now carry measured numbers. One hypothesis that did not survive testingCodeGen pads Also verified: the safetensors converter's output loads and gives byte-identical greedy output to the pickle path. 102 tests pass, clippy clean. |
GitMind PR Review{'pr_url': '#15', 'repo': 'Ayyankhan101/Transformer-In-Rust', 'pr_num': '15', 'title': 'Fix CodeGen inference, make it fast, make training train', 'author': 'Ayyankhan101', 'additions': 2060, 'deletions': 1603, 'changed_files': 51, 'review': '## Summary\nAutomatic review could not be generated — please review the diff manually.\n\n## Issues\nNo significant issues found.\n\n## Verdict\n💬 NEEDS DISCUSSION\nThe automated review failed; human review required.'} Auto-generated by GitMind — AI-powered code analysis. |
Three passes over this repo. Each found the same shape of defect: code that runs, prints
plausible numbers, and does the wrong thing or nothing at all. Each is now covered by a
check that did not exist before.
1. CodeGen inference was incorrect
The model could not produce valid output before this.
qkv_projtensor was split wrong. The code assumed[all q | all v | all k]. CodeGen keeps the sharded layout of the original TPUimplementation: four interleaved groups of
[q | v | k](mp_num = 4upstream). Everyattention head was reading a different slice of the projection than it should.
u64state byu32::MAX,producing values around 6e9, so the selection loop always fell through to argmax —
temperature,top_kandtop_pdid nothing.complete,replandPOST /generatesliced off the wrong tokens, strippingprompt.len()entries from a generated-only vector: dropped output, or a panic when theprompt was longer than the completion.
config.jsonand--f16, loading withCodeGenConfig::default()—rotary_dim64 rather than the checkpoint's 32.nonsense rather than an error.
new_blankhardcoded F32 regardless ofconfig.dtype.tests/codegen_parity.rsnow checks logits against the HuggingFace reference usingcommitted tiny-model fixtures, so it needs no weight download. It fails at a max logit
difference of 0.66 against the old QKV split and passes at 4e-7 against the new one.
2. Performance
Measured on the benchmark model:
KVCache::appendusedslice_assign, which is not a targeted write — candle zero-pads thesource to the full buffer shape, allocates a full-size mask, pads that too, then runs
where_condacross every element, forkandv, in every layer, for every token.slice_setcopies only the new tokens in place.lm_headno longer runs over the whole prompt when only the final position is read: 6.0 msof a 35.8 ms prefill.
benches/transformer.rsnow benchmarks this crate. Every benchmark used to be ahand-inlined copy of the model, carrying the QKV bug fixed above, so the suite could report
neither a speedup nor a regression.
The CI lint gate was already failing.
main.rsre-declared the whole module treeinstead of using the library, so the binary compiled a second copy and every
pubitem theCLI did not call was reported as dead code — 34 warnings from one cause.
cargo clippy --all-targets --all-features -- -D warningsgoes from exit 101 to exit 0.Removed
src/codegen/quantized.rs: never constructed outside its own tests, and itsforwarddequantized the entire weight matrix per call, making it strictly slower than theF32 path it would have replaced. README, docs and the comparison table no longer claim it.
Added safetensors loading, so the converter's output is finally loadable.
3.
glm-traindid not trainBoth cross-entropy paths summed into an
f32and returnedTensor::new(total, device)— afresh leaf with no autograd history. Backprop from a constant yields an empty gradient
store, so AdamW updated nothing and every step was a no-op. The
Varwiring was correct;only the loss was detached.
A real 80-step run now falls from loss 10.80 to 4.99. Before the fix, 60 optimizer steps
left the loss at 4.1934047 both before and after, and 0 of 19 parameters received gradients.
Found while verifying that:
save_safetensorscalledto_vec1::<u8>(), which errorson rank > 1, so runs ended in
unexpected rank, expected: 1, got: 2.configs/train.yamldid not deserialize:eval_stepssat under a separateevaluation:section and
tokenizer_pathwas missing. Nothing had ever loaded the file becauseglm-trainhad no--configflag. Added the flag, plus#[serde(default)]so partialconfigs fall back field by field.
gradient_accumulation_stepsandmax_grad_normwere declared and claimed as delivered,but read nowhere. Both are implemented now.
optimizer.step, so each step ran on the previousstep's rate.
src/training/checkpoint.rs: 192 lines never declared inmod.rs, so nevercompiled, duplicating the checkpoint code in
train.rs.The README and
docs/train-code-infill.mdcarried a fabricated training log — inventedlosses and per-step timings, in a format the code never emitted. Replaced with real output.
Verification
Known gap
Nothing here has run against the real 350M checkpoint —
codegen_weights/pytorch_model.binhas never been present, so both tests intests/codegen_integration.rsself-skip. The parity fixtures share CodeGen's architecturebut exercise 2 layers and a 256-token vocabulary. That validation is the next pass.