perf: vectorize per-sample logit slicing to reduce KL kernel launches (issue #6 item 3) - #20
Conversation
ReviewThe vectorization approach is correct — building padded batch tensors and calling 1. No new tests for a numerical refactor. This changes the core loss computation path. Please add at least one test that verifies numerical equivalence between old (per-sample loop) and new (batched) approaches on a fixed seed. A regression here would be silent and catastrophic. 2. Duplicated logic between Minor: Please add the equivalence test and consider the dedup. Happy to merge once the test is in. |
… (issue marksverdhei#6 item 3) The loss computation loop called compute_kl_divergence N times (once per sample per batch), each requiring its own softmax + log_softmax + kl_div kernel launch. For batch size B, this is 3*B separate CUDA kernel calls. Replace with a single batched call: 1. Compute per-sample response start positions (CPU, cheap) 2. Build padded batch tensors [valid, max_resp_len, V] via CPU loop (copies only response slices — no wasted full-sequence allocation) 3. Call compute_kl_divergence once with per_sample=True 4. Average the per-sample losses The same vectorization is applied to the sequential_eval path in prediction_step to keep both code paths consistent. For batch size B=8 this reduces CUDA kernel launches from 24 (8 × 3) to 3 (1 × 3), at the cost of one extra CPU loop to build the aligned batch tensors. Net effect: fewer GPU-CPU round trips, better GPU utilization, especially for larger batch sizes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lence test Address PR marksverdhei#20 review feedback: 1. Add test_batched_kl_matches_per_sample_loop that verifies the batched KL computation matches a per-sample reference loop on fixed-seed data. 2. Extract duplicated logic between compute_loss and prediction_step into shared helpers: _prepare_pairs, _build_texts_and_lengths, _make_fwd_kwargs, and _compute_batched_kl. 3. Simplify t_seq_len - 1 - (t_starts[i] - 1) to t_seq_len - t_starts[i]. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ac420a6 to
60aa639
Compare
marksverdhei
left a comment
There was a problem hiding this comment.
Reviewed. All requested changes addressed:
-
Numerical equivalence test added — test_batched_kl_matches_per_sample_loop verifies the batched path matches a manual per-sample loop on fixed seed. This is the critical safety net for this refactor.
-
Deduplication done — Extracted _compute_batched_kl, _prepare_pairs, _build_texts_and_lengths, _make_fwd_kwargs as shared helpers. compute_loss and prediction_step now both delegate to these, eliminating the copy-paste.
-
Simplified arithmetic — t_seq_len - t_starts[i] instead of the longer form.
The vectorized approach (building padded batch tensors, single compute_kl_divergence call with per_sample=True) is correct and well-commented. LGTM.
Summary
Addresses issue #6 item 3: "Vectorize per-sample logit slicing loop".
The
compute_lossloop calledcompute_kl_divergenceN times per batch (once per sample), each triggering 3 separate CUDA kernel launches (softmax + log_softmax + kl_div). For batch size B=8, that's 24 kernel calls per training step.This PR reduces it to 3 kernel calls per training step by:
[valid, max_resp_len, V]via a CPU loop (copies only response slices)compute_kl_divergenceonce withper_sample=TrueThe same vectorization is applied to the
sequential_evalpath inprediction_stepfor consistency.Why it's safe
The aligned batch tensors contain exactly the same data as the per-sample slices — no numerical change to the loss. The
per_sample=TrueKL path was already tested and exists inkl.py.Test plan
uv run pytest tests/ -k "not benchmark")test_compute_loss_returns_scalar— loss is still scalar with gradienttest_loss_is_differentiable— LoRA params still receive gradientstest_compute_loss_batch— batch padding logic still correcttest_compute_loss_empty_inputs/test_compute_loss_empty_responses— zero-loss paths work🤖 Generated with Claude Code