Skip to content

Avoid repeated reallocation of the TopK buffer - #1011

Merged
copybara-service[bot] merged 1 commit into
google:devfrom
Magnushst:perf/topk-reserve
Sep 4, 2026
Merged

Avoid repeated reallocation of the TopK buffer#1011
copybara-service[bot] merged 1 commit into
google:devfrom
Magnushst:perf/topk-reserve

Conversation

@Magnushst

Copy link
Copy Markdown

Summary

TopK fills a std::vector<double> with one packed entry per logit but starts from an empty vector. logits.size() is the vocabulary size, so for Gemma 3's 256K vocabulary libstdc++ reallocates 19 times and copies 2.1 MB before the buffer reaches its final size. This runs once per sampled token via FusedSoftmaxAndSampleTopK, which ChooseSampleFunc selects whenever top_k is greater than 1 or an accept_token filter is set.

This reserves the full size up front and hoists the accept_token test out of the loop, so the common case (no constrained decoding) is a straight-line pack-and-append rather than a std::function test per logit. Peak transient memory also falls, because the final doubling no longer holds the old and new buffers at the same time.

Sampling results are unchanged: the same entries are appended in the same order, so the subsequent VQSelect/VQSort see identical input.

Performance

Deterministic reduction, for a 256K vocabulary:

  • Allocations per call: 19 -> 1
  • Bytes copied by reallocation: 2.10 MB -> 0
  • Peak transient buffer: 3 MB -> 2 MB

Wall time, interleaved baseline/patched runs, best of 30 calls (300 for the small cases), median of three runs:

Case Baseline Patched Change
vocab 262144, k=50 366.7 us 284.0 us -22.6%
vocab 32768, k=50 43.4 us 33.3 us -23.3%
vocab 256, k=3 0.63 us 0.51 us -19%
vocab 262144, k=50, accept_token 485.2 us 461.4 us neutral (within noise)

The accept_token case is the control: it keeps the per-element filter call, which dominates, so it is unaffected.

Environment:

  • CPU: Intel Core i9-13905H
  • Compiler: GCC 14.2.0
  • Build: CMake Release, Ninja, -O3 -DNDEBUG
  • Highway target: AVX2 (best attainable on this CPU)
  • Repetitions: best of 30 per run, three interleaved runs, pinned with taskset

This is a sampling-path improvement, not an end-to-end decode speedup; on a memory-bound decode step the saving is small next to the output-head MatMul.

Testing

  • New TestTopK covers the filtered and unfiltered paths, asserting exact tokens and probabilities. Its size exceeds the vector's initial capacity, so it exercises the reserved-capacity path.
  • ops_test passes 40/40 across AVX2 and EMU128; compress_test and sfp_test unaffected.
  • ops_test.cc built with -fsanitize=address and run with detect_leaks=1: 40/40 pass, no errors and no leaks.

Notes

Only x86-64 (AVX2) was measured; ARM was not benchmarked. The change is allocation behaviour rather than SIMD, so it should carry over, but I have not verified that.

@Magnushst
Magnushst changed the base branch from main to dev September 3, 2026 17:01

@jan-wassenberg jan-wassenberg left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice improvements, thank you!

@jan-wassenberg jan-wassenberg added the copybara-import Trigger Copybara for merging pull requests label Sep 3, 2026
@jan-wassenberg

Copy link
Copy Markdown
Member

hm, we have an issue with the CLA scanner because there are too many commits. I suspect this is caused by initially targeting the main branch and then rebasing to dev. Anything we can do to get a clean set of commits?

`TopK` fills a `std::vector<double>` with one packed entry per logit, but
starts from an empty vector. `logits.size()` is the vocabulary size, so for
Gemma 3's 256K vocabulary libstdc++ reallocates 19 times and copies 2.1 MB
before the buffer reaches its final size. This happens on every sampled token
whenever `--top_k` is greater than 1.

Reserve the full size up front, and hoist the `accept_token` test out of the
loop so that the common case (no constrained decoding) is a straight-line
pack-and-append. Peak transient memory also falls, because the final doubling
no longer holds the old and new buffers at the same time.

Sampling results are unchanged: the same entries are appended in the same
order, so the subsequent VQSelect/VQSort see identical input.

Measured on an i9-13905H (AVX2, GCC 14.2, -O3 -DNDEBUG), best of 30 calls,
median of three interleaved baseline/patched runs:

  vocab 256K, k=50   367 us -> 284 us  (-23%)
  vocab  32K, k=50  43.4 us -> 33.3 us (-23%)

Add `TestTopK`, which covers both the filtered and the unfiltered path.
@Magnushst

Copy link
Copy Markdown
Author

hm, we have an issue with the CLA scanner because there are too many commits. I suspect this is caused by initially targeting the main branch and then rebasing to dev. Anything we can do to get a clean set of commits?

Yeah, that is exactly why I rebased; was a bit confused at first when I saw all the commits. I'll try force pushing again and see if that resolves it

@copybara-service
copybara-service Bot merged commit dd04362 into google:dev Sep 4, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

copybara-import Trigger Copybara for merging pull requests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants