Skip to content

perf(utils): simpler and faster numpy padding fallback, test numba in CI - #11

Merged
AmitMY merged 1 commit into
mainfrom
perf/simpler-faster-padding-fallback
Jul 27, 2026
Merged

perf(utils): simpler and faster numpy padding fallback, test numba in CI#11
AmitMY merged 1 commit into
mainfrom
perf/simpler-faster-padding-fallback

Conversation

@AmitMY

@AmitMY AmitMY commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Audit item #1 — but the conclusion is the opposite of what that audit proposed, now that I have measurements.

The audit was wrong about numba; here are the numbers

I originally suspected numba was an unnecessary dependency. With it actually installed, it wins decisively:

case numba numpy fallback (old) simple loop
16 short 2.4 µs 7.2 7.1
64 mixed 4.6 µs 37.1 25.1
256 mixed 16.3 µs 321.2 97.0
8 very ragged 7.2 35.2 4.9

5–20× faster, one-time JIT compile 179 ms. The fast extra earns its place. Retracting that part of the audit.

What actually needed fixing: the fallback

The no-numba path built per-element row and column index arrays (repeat / cumsum / searchsorted) to perform a single vectorised scatter. Filling row by row is shorter and much faster — numpy already copies each row at C speed, so the index arrays were pure overhead:

-        batch_size = len(lengths)
-        row_indices = np.repeat(np.arange(batch_size), lengths)
-        cumsum = lengths.cumsum()
-        positions = np.arange(len(all_values))
-        groups = np.searchsorted(cumsum, positions, side='right')
-        prev_cumsum = np.empty(batch_size, dtype=np.uint32)
-        prev_cumsum[0] = 0
-        prev_cumsum[1:] = cumsum[:-1]
-        col_indices = positions - prev_cumsum[groups]
-        output[row_indices, col_indices] = all_values
+        offset = 0
+        for row, length in enumerate(lengths):
+            output[row, :length] = all_values[offset:offset + length]
+            offset += length

4.7× faster at 256 rows (321 → 69 µs), 7× on ragged input (35 → 4.9 µs), 15 lines → 6.

The bigger find: the fast path was never tested

CI ran uv pip install ".[dev]" and never ".[fast]". So numba — the path this package advertises for speed — was not executed by CI at all. Added an extras axis so both padding paths run against both transformers versions:

        transformers: ["<5", ">=5"]
        # "dev" exercises the pure-numpy padding path, "dev,fast" the numba one
        extras: ["dev", "dev,fast"]

Four jobs instead of two.

README

Since numba is a 5–20× win, it should not be buried in an optional extra nobody discovers:

pip install "utf8-tokenizer[fast]"

...with a note that the plain install still works and falls back to numpy.

One honest caveat

The new fallback is algorithmically close to pad_bytearrays_to_tensor_loop, which test_utils uses as a reference — so in the plain dev leg, those 16 comparisons are less independent than before. Two things keep it honest: the new dev,fast leg means pad_bytearrays_to_tensor goes through numba there, making the comparison meaningful again, and test_utils has 9 assertions against an independent torch.nn.utils.rnn.pad_sequence reference that cover the plain leg.

Verification

  • 241 passed with numba installed and 241 passed without it (locally: numba+transformers 5.14.1, and no-numba+transformers 4.57.6).
  • All three implementations asserted byte-identical on every benchmark case before timing.
  • ruff check . clean.

🤖 Generated with Claude Code


Note

Low Risk
Performance and test-matrix changes in padding utilities and docs; no auth or API contract changes.

Overview
Replaces the no-numba padding fallback in _fill_padded with a short row-by-row copy loop instead of building scatter index arrays, improving performance when numba is not installed.

CI now runs four jobs (transformers <5 / >=5 × dev / dev,fast) so tests exercise both the pure-numpy and numba padding paths; install uses ".[${{ matrix.extras }}]".

README recommends pip install "utf8-tokenizer[fast]" and notes that the plain install still works with the numpy fallback.

Reviewed by Cursor Bugbot for commit 465043b. Bugbot is set up for automated code reviews on this repo. Configure here.

The no-numba fallback built per-element row and column index arrays via
repeat/cumsum/searchsorted to do one vectorised scatter. Filling row by row
is both shorter and much faster, because numpy already copies each row at C
speed and the index arrays were pure overhead:

  16 short          7.2 us -> 6.4 us
  64 mixed         37.1 us -> 18.9 us
  256 mixed       321.2 us -> 68.8 us
  8 very ragged    35.2 us -> 4.9 us

CI installed ".[dev]", never ".[fast]", so the numba path -- the fast one
this package advertises -- was never executed in CI at all. Added an extras
axis to the matrix so both padding paths run against both transformers
versions.

Measured numba against the fallback to confirm the extra is worth keeping,
since an earlier audit pass suspected it was not: numba wins decisively
(16.3 us vs 68.8 us at 256 rows, 4.6 us vs 18.9 us at 64), one-time JIT
compile 179 ms. So the README now recommends the [fast] extra rather than
burying it.

Note the fallback is now algorithmically close to
pad_bytearrays_to_tensor_loop, which tests use as a reference. The new
"dev,fast" CI leg keeps that comparison meaningful, and the independent
torch pad_sequence reference in test_utils covers the plain leg.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AmitMY
AmitMY merged commit 41b5478 into main Jul 27, 2026
6 checks passed
@AmitMY
AmitMY deleted the perf/simpler-faster-padding-fallback branch July 27, 2026 11:21
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.

1 participant