Skip to content

perf: vectorize per-sample logit slicing to reduce KL kernel launches (issue #6 item 3) - #20

Merged
marksverdhei merged 3 commits into
marksverdhei:mainfrom
hai-pilgrim:perf/vectorize-logit-loop
Mar 29, 2026
Merged

marksverdhei merged 3 commits into
marksverdhei:mainfrom
hai-pilgrim:perf/vectorize-logit-loop

Conversation

@hai-pilgrim

Copy link
Copy Markdown

Summary

Addresses issue #6 item 3: "Vectorize per-sample logit slicing loop".

The compute_loss loop called compute_kl_divergence N 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:

  1. Computing per-sample response start positions (CPU, ~microseconds)
  2. Building padded batch tensors [valid, max_resp_len, V] via a CPU loop (copies only response slices)
  3. Calling compute_kl_divergence once with per_sample=True
  4. Averaging per-sample losses

The same vectorization is applied to the sequential_eval path in prediction_step for 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=True KL path was already tested and exists in kl.py.

Test plan

  • All existing tests pass (uv run pytest tests/ -k "not benchmark")
  • test_compute_loss_returns_scalar — loss is still scalar with gradient
  • test_loss_is_differentiable — LoRA params still receive gradients
  • test_compute_loss_batch — batch padding logic still correct
  • test_compute_loss_empty_inputs / test_compute_loss_empty_responses — zero-loss paths work

🤖 Generated with Claude Code

@marksverdhei

Copy link
Copy Markdown
Owner

Review

The vectorization approach is correct — building padded batch tensors and calling compute_kl_divergence once with per_sample=True instead of looping per-sample. Two issues:

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 compute_loss and prediction_step. The vectorization pattern (start computation, valid filtering, batch assembly, KL call) is copy-pasted between both methods. Consider extracting it into a shared helper like _compute_batched_kl(teacher_outputs, student_outputs, teacher_inputs, student_inputs, teacher_prompt_lengths, student_prompt_lengths).

Minor: t_seq_len - 1 - (t_starts[i] - 1) simplifies to t_seq_len - t_starts[i].

Please add the equivalence test and consider the dedup. Happy to merge once the test is in.

marksverdhei and others added 2 commits March 29, 2026 17:48
… (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>
@hai-pilgrim
hai-pilgrim force-pushed the perf/vectorize-logit-loop branch from ac420a6 to 60aa639 Compare March 29, 2026 17:51

@marksverdhei marksverdhei left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. All requested changes addressed:

  1. 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.

  2. 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.

  3. 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.

@marksverdhei
marksverdhei merged commit e9837a4 into marksverdhei:main Mar 29, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants