Skip to content

feat(python): add Arrow string batch tokenization - #2400

Open
granthamtaylor wants to merge 1 commit into
huggingface:mainfrom
granthamtaylor:feat/python-arrow-batch-tokenization
Open

feat(python): add Arrow string batch tokenization#2400
granthamtaylor wants to merge 1 commit into
huggingface:mainfrom
granthamtaylor:feat/python-arrow-batch-tokenization

Conversation

@granthamtaylor

Copy link
Copy Markdown

Arrow callers currently need to materialize Python strings before calling encode_batch. This adds Tokenizer.encode_batch_arrow, which accepts UTF-8 string and large_string arrays through __arrow_c_array__ and returns the existing Encoding objects. It supports slices and empty arrays and preserves tokenizer configuration and character offsets.

The Python binding uses arrow-array and arrow-schema for C Data Interface ownership and validation. Imported buffers remain alive throughout synchronous parallel encoding, and release callbacks run after reattaching to Python. Non-null text is checked for valid UTF-8; null payload bytes are left uninterpreted. PyArrow is an explicit test dependency, with no new Python runtime dependency.

Null values raise an error by default. The keyword-only null_handling="empty" option encodes them as empty strings, and null_handling="skip" omits them while preserving retained row order. Generated stubs expose these choices as Literal["error", "empty", "skip"], with runtime validation retained. Inputs are flat string arrays with offsets aligned for their native type; nested and chunked arrays, sequence pairs, and pre-tokenized inputs are outside this method's scope.

Includes API documentation, tests for encoding parity and capsule ownership, and a standalone benchmark. The benchmark compares list conversion and Arrow input in fresh processes, reports timing and absolute process peak RSS separately, and retains raw samples and environment details. Tokenization and output construction can still allocate memory; no general performance improvement is claimed from the synthetic benchmark.

Python throughput measurements cover 64 configurations: batch sizes 1, 16, 256 and 4,096; body lengths 32, 256, 2,048 and 8,192 Unicode characters; both Arrow string widths; and one or four Rayon threads. Each configuration has five paired repetitions, for 640 fresh worker processes.

For 32-character bodies, single-item calls ran at 0.89–0.91x the list baseline. With four threads, batches of 16–4,096 ran at 1.07–1.13x. One-thread cases with 2,048- or 8,192-character bodies were near parity (0.998–1.024x). For example, at 4,096 rows × 32 characters with four threads, string throughput was 551,025 documents/s for the list path and 601,467 documents/s for Arrow, with a median paired ratio of 1.07x. These measurements show workload-dependent gains and small-call overhead.

Measured on an Apple M4 Pro (14 cores, 48 GiB), macOS 26.5.2 ARM64, Python 3.13.4, PyArrow 25.0.1, NumPy 2.5.3 and pandas 3.0.5. Both paths used the same release build of this Arrow patch on base 6cfd9d385ca0ed91c10b49f0ce97d02cfde1b607, with Rust 1.98.1 and the default Python binding features. This was a shared development machine without CPU affinity.

The fixture uses WordLevel with Whitespace, repeating "hello world arrow café 東京 " to the requested length. The four body lengths occupy 37, 302, 2,438 and 9,767 UTF-8 bytes respectively. All rows in a batch have the same body; there are no nulls, padding or truncation. This is a synthetic measurement of repeated calls and does not establish performance for BPE, other text distributions or complete applications.

Each fresh worker warms up one full batch, then measures calls until cumulative timed duration reaches at least 0.2 seconds. Timings include array.to_pylist() plus encode_batch for the list path, and encode_batch_arrow for the Arrow path, including Python dispatch, Arrow import/validation, tokenization and returned Encoding construction. Fixture setup, warmup, correctness checks, previous input/output destruction and loop bookkeeping are outside the timers. Workers run sequentially; list/Arrow order alternates across paired repetitions and cases.

The following 48 matrices cover all 12 recorded metrics, with List / Arrow values for each of the 64 configurations. Timings and throughput summarize per-worker mean calls; RSS, counts and cumulative duration summarize their corresponding process or batch observations. The setup RSS and workload/call-count matrices provide context, not independent measures of performance.

Medians are calculated independently for each metric: median conversion plus median encoding need not equal median total, although each raw sample's components sum to its total. Arrow's explicit Python conversion time is zero; its native import and validation are included in encoding time. Peak RSS includes imports, fixtures, full-batch warmup and outputs. For example, the 4,096 × 8,192-character string case with four threads reached 1,029.0 MiB for the list path and 830.6 MiB for Arrow. These are whole-process high-water marks, not allocation deltas.

Conversion time (ms/call)

Conversion time (ms/call)

Every cell: List / Arrow, median across five workers. Lower is better for this measured stage.

List measures to_pylist(); Arrow is exactly 0 because it does not run this conversion. Arrow import and validation costs are included in encoding time, not zero overall overhead.

Timing medians are independent; their components need not sum after aggregation.

string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 0.000139665 / 0 0.000279487 / 0 0.00132899 / 0 0.00453535 / 0
16 0.00167555 / 0 0.00431082 / 0 0.0194646 / 0 0.0755326 / 0
256 0.0192046 / 0 0.0556163 / 0 0.310048 / 0 1.19256 / 0
4,096 0.293397 / 0 0.819681 / 0 4.67858 / 0 17.9527 / 0

string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 0.000141483 / 0 0.000304659 / 0 0.00133828 / 0 0.00471135 / 0
16 0.00179057 / 0 0.00437794 / 0 0.0199815 / 0 0.0706699 / 0
256 0.0194049 / 0 0.0564428 / 0 0.30731 / 0 1.15973 / 0
4,096 0.292423 / 0 0.87049 / 0 4.97625 / 0 18.6844 / 0

large_string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 0.000143998 / 0 0.000298678 / 0 0.00137247 / 0 0.0048069 / 0
16 0.00169124 / 0 0.00442209 / 0 0.0199543 / 0 0.0749023 / 0
256 0.0194759 / 0 0.056629 / 0 0.311632 / 0 1.11792 / 0
4,096 0.290292 / 0 0.846917 / 0 4.54771 / 0 17.9538 / 0

large_string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 0.000144282 / 0 0.000299828 / 0 0.00134561 / 0 0.00474003 / 0
16 0.00180321 / 0 0.00430534 / 0 0.0201861 / 0 0.0720319 / 0
256 0.0198658 / 0 0.0568548 / 0 0.324153 / 0 1.19139 / 0
4,096 0.298517 / 0 0.870651 / 0 4.99096 / 0 18.5409 / 0

List = array.to_pylist() + encode_batch(); Arrow = encode_batch_arrow(). Native import/validation belongs to Arrow encoding time. Full precision is retained in the saved CSV and JSON results.

Encoding time (ms/call)

Encoding time (ms/call)

Every cell: List / Arrow, median across five workers. Lower is better for the defined encoding stage.

List excludes its explicit conversion; Arrow includes native import and validation. Use total time to compare the complete measured calls.

Timing medians are independent; their components need not sum after aggregation.

string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 0.00353421 / 0.00406631 0.0178252 / 0.0185093 0.131367 / 0.130429 0.523824 / 0.519749
16 0.0611137 / 0.0619874 0.29715 / 0.297202 2.12483 / 2.11893 8.62774 / 8.51788
256 0.841268 / 0.828713 4.6381 / 4.65797 34.2705 / 34.2277 139.064 / 137.035
4,096 13.8649 / 13.2399 75.1199 / 74.5748 560.625 / 555.702 2,261.55 / 2,270.64

string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 0.00361662 / 0.00421991 0.018707 / 0.0194354 0.1363 / 0.135897 0.533205 / 0.536531
16 0.0517156 / 0.0476926 0.13573 / 0.132596 0.695245 / 0.705417 2.58038 / 2.55283
256 0.463096 / 0.445944 1.62801 / 1.56834 10.914 / 11.2154 44.1012 / 44.0482
4,096 7.141 / 6.81001 26.2351 / 25.466 186.392 / 179.643 719.574 / 709.78

large_string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 0.00366207 / 0.00425551 0.0185219 / 0.0192002 0.134973 / 0.135533 0.541237 / 0.531716
16 0.0629416 / 0.0622305 0.309958 / 0.301714 2.17346 / 2.17389 8.69676 / 8.69701
256 0.858132 / 0.849768 4.69411 / 4.68981 34.8201 / 34.5405 138.724 / 139.126
4,096 13.8632 / 13.6841 76.0324 / 76.9828 550.679 / 549.485 2,234.01 / 2,259.61

large_string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 0.00364678 / 0.00425966 0.0185968 / 0.019217 0.135356 / 0.135793 0.541025 / 0.535897
16 0.0511195 / 0.0462613 0.134024 / 0.131853 0.698568 / 0.704293 2.58504 / 2.57029
256 0.471481 / 0.449507 1.62372 / 1.59107 11.0739 / 10.8916 45.1536 / 44.2749
4,096 7.26769 / 6.85306 26.4682 / 25.8646 179.365 / 176.162 698.985 / 684.068

List = array.to_pylist() + encode_batch(); Arrow = encode_batch_arrow(). Native import/validation belongs to Arrow encoding time. Full precision is retained in the saved CSV and JSON results.

Total call time (ms/call)

Total call time (ms/call)

Every cell: List / Arrow, median across five workers. Lower is better.

Includes explicit conversion where applicable and the call returning Encoding objects.

Timing medians are independent; their components need not sum after aggregation.

string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 0.00367795 / 0.00406631 0.0181016 / 0.0185093 0.132761 / 0.130429 0.528545 / 0.519749
16 0.0627892 / 0.0619874 0.301461 / 0.297202 2.14429 / 2.11893 8.70413 / 8.51788
256 0.860445 / 0.828713 4.69435 / 4.65797 34.5732 / 34.2277 140.379 / 137.035
4,096 14.1583 / 13.2399 75.9871 / 74.5748 566.048 / 555.702 2,279.5 / 2,270.64

string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 0.00376468 / 0.00421991 0.0190051 / 0.0194354 0.137636 / 0.135897 0.537916 / 0.536531
16 0.0535046 / 0.0476926 0.140019 / 0.132596 0.715226 / 0.705417 2.65105 / 2.55283
256 0.482515 / 0.445944 1.6845 / 1.56834 11.2201 / 11.2154 45.2423 / 44.0482
4,096 7.43342 / 6.81001 27.0833 / 25.466 191.445 / 179.643 738.631 / 709.78

large_string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 0.00380101 / 0.00425551 0.0188219 / 0.0192002 0.136345 / 0.135533 0.546044 / 0.531716
16 0.0646328 / 0.0622305 0.31438 / 0.301714 2.19341 / 2.17389 8.77166 / 8.69701
256 0.87702 / 0.849768 4.75088 / 4.68981 35.1249 / 34.5405 139.797 / 139.126
4,096 14.1535 / 13.6841 76.8876 / 76.9828 554.882 / 549.485 2,252.35 / 2,259.61

large_string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 0.00379173 / 0.00425966 0.0188994 / 0.019217 0.136671 / 0.135793 0.545686 / 0.535897
16 0.0529319 / 0.0462613 0.138329 / 0.131853 0.719271 / 0.704293 2.65646 / 2.57029
256 0.491364 / 0.449507 1.68101 / 1.59107 11.3955 / 10.8916 46.345 / 44.2749
4,096 7.56621 / 6.85306 27.3442 / 25.8646 184.406 / 176.162 716.719 / 684.068

List = array.to_pylist() + encode_batch(); Arrow = encode_batch_arrow(). Native import/validation belongs to Arrow encoding time. Full precision is retained in the saved CSV and JSON results.

Document throughput (documents/s)

Document throughput (documents/s)

Every cell: List / Arrow, median across five workers. Higher is better.

Derived from each worker's mean total call time, then summarized across workers.

Timing medians are independent; their components need not sum after aggregation.

string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 271,891 / 245,923 55,244 / 54,027 7,532 / 7,667 1,892 / 1,924
16 254,821 / 258,117 53,075 / 53,835 7,462 / 7,551 1,838 / 1,878
256 297,520 / 308,913 54,534 / 54,960 7,405 / 7,479 1,824 / 1,868
4,096 289,300 / 309,367 53,904 / 54,925 7,236 / 7,371 1,797 / 1,804

string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 265,627 / 236,972 52,618 / 51,453 7,266 / 7,359 1,859 / 1,864
16 299,040 / 335,482 114,270 / 120,667 22,371 / 22,682 6,035 / 6,268
256 530,553 / 574,063 151,974 / 163,230 22,816 / 22,826 5,658 / 5,812
4,096 551,025 / 601,467 151,237 / 160,842 21,395 / 22,801 5,545 / 5,771

large_string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 263,088 / 234,989 53,130 / 52,083 7,334 / 7,378 1,831 / 1,881
16 247,552 / 257,109 50,894 / 53,030 7,295 / 7,360 1,824 / 1,840
256 291,898 / 301,259 53,885 / 54,586 7,288 / 7,412 1,831 / 1,840
4,096 289,398 / 299,325 53,273 / 53,207 7,382 / 7,454 1,819 / 1,813

large_string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 263,732 / 234,761 52,912 / 52,037 7,317 / 7,364 1,833 / 1,866
16 302,275 / 345,861 115,666 / 121,348 22,245 / 22,718 6,023 / 6,225
256 520,998 / 569,513 152,290 / 160,898 22,465 / 23,504 5,524 / 5,782
4,096 541,354 / 597,690 149,794 / 158,363 22,212 / 23,251 5,715 / 5,988

List = array.to_pylist() + encode_batch(); Arrow = encode_batch_arrow(). Native import/validation belongs to Arrow encoding time. Full precision is retained in the saved CSV and JSON results.

UTF-8 throughput (MiB/s)

UTF-8 throughput (MiB/s)

Every cell: List / Arrow, median across five workers. Higher is better.

Uses actual UTF-8 input bytes and total call time; 1 MiB = 1,048,576 bytes.

Timing medians are independent; their components need not sum after aggregation.

string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 9.59 / 8.68 15.91 / 15.56 17.51 / 17.83 17.62 / 17.92
16 8.99 / 9.11 15.29 / 15.51 17.35 / 17.56 17.12 / 17.50
256 10.50 / 10.90 15.71 / 15.83 17.22 / 17.39 16.99 / 17.40
4,096 10.21 / 10.92 15.52 / 15.82 16.82 / 17.14 16.74 / 16.80

string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 9.37 / 8.36 15.15 / 14.82 16.89 / 17.11 17.32 / 17.36
16 10.55 / 11.84 32.91 / 34.75 52.01 / 52.74 56.22 / 58.38
256 18.72 / 20.26 43.77 / 47.01 53.05 / 53.07 52.71 / 54.13
4,096 19.44 / 21.22 43.56 / 46.32 49.74 / 53.01 51.65 / 53.75

large_string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 9.28 / 8.29 15.30 / 15.00 17.05 / 17.15 17.06 / 17.52
16 8.74 / 9.07 14.66 / 15.27 16.96 / 17.11 16.99 / 17.14
256 10.30 / 10.63 15.52 / 15.72 16.95 / 17.23 17.06 / 17.14
4,096 10.21 / 10.56 15.34 / 15.32 17.16 / 17.33 16.94 / 16.88

large_string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 9.31 / 8.28 15.24 / 14.99 17.01 / 17.12 17.07 / 17.38
16 10.67 / 12.20 33.31 / 34.95 51.72 / 52.82 56.10 / 57.98
256 18.38 / 20.10 43.86 / 46.34 52.23 / 54.65 51.45 / 53.86
4,096 19.10 / 21.09 43.14 / 45.61 51.64 / 54.06 53.23 / 55.77

List = array.to_pylist() + encode_batch(); Arrow = encode_batch_arrow(). Native import/validation belongs to Arrow encoding time. Full precision is retained in the saved CSV and JSON results.

Output token throughput (tokens/s)

Output token throughput (tokens/s)

Every cell: List / Arrow, median across five workers. Higher is better.

Uses returned output token counts and total call time.

Timing medians are independent; their components need not sum after aggregation.

string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 1,631,345 / 1,475,539 2,706,938 / 2,647,320 2,967,747 / 3,020,801 2,981,773 / 3,032,234
16 1,528,925 / 1,548,702 2,600,670 / 2,637,933 2,939,900 / 2,975,081 2,897,016 / 2,960,360
256 1,785,123 / 1,853,476 2,672,150 / 2,693,020 2,917,405 / 2,946,853 2,874,055 / 2,944,173
4,096 1,735,800 / 1,856,202 2,641,290 / 2,691,313 2,851,039 / 2,904,118 2,831,893 / 2,842,943

string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 1,593,763 / 1,421,831 2,578,261 / 2,521,178 2,862,624 / 2,899,265 2,929,824 / 2,937,387
16 1,794,238 / 2,012,889 5,599,232 / 5,912,678 8,813,993 / 8,936,553 9,511,692 / 9,877,647
256 3,183,321 / 3,444,380 7,446,731 / 7,998,286 8,989,596 / 8,993,318 8,917,680 / 9,159,414
4,096 3,306,149 / 3,608,803 7,410,618 / 7,881,263 8,429,684 / 8,983,495 8,739,543 / 9,094,789

large_string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 1,578,527 / 1,409,937 2,603,353 / 2,552,057 2,889,718 / 2,907,049 2,886,215 / 2,963,990
16 1,485,313 / 1,542,652 2,493,800 / 2,598,483 2,874,059 / 2,899,873 2,874,711 / 2,899,386
256 1,751,386 / 1,807,552 2,640,353 / 2,674,736 2,871,579 / 2,920,167 2,886,004 / 2,899,924
4,096 1,736,388 / 1,795,950 2,610,357 / 2,607,127 2,908,412 / 2,936,978 2,866,024 / 2,856,824

large_string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 1,582,393 / 1,408,564 2,592,669 / 2,549,822 2,882,834 / 2,901,473 2,888,105 / 2,940,862
16 1,813,651 / 2,075,167 5,667,642 / 5,946,032 8,764,434 / 8,950,815 9,492,316 / 9,810,584
256 3,125,990 / 3,417,077 7,462,196 / 7,884,010 8,851,242 / 9,260,675 8,705,491 / 9,112,513
4,096 3,248,127 / 3,586,138 7,339,917 / 7,759,802 8,751,487 / 9,161,034 9,006,736 / 9,436,624

List = array.to_pylist() + encode_batch(); Arrow = encode_batch_arrow(). Native import/validation belongs to Arrow encoding time. Full precision is retained in the saved CSV and JSON results.

Process peak resident memory (MiB)

Process peak resident memory (MiB)

Every cell: List / Arrow, median across five workers. Lower absolute process peak is preferable.

Median process high-water mark through encoding, including imports, fixture setup, warmup and outputs. This is not isolated tokenizer allocation; do not subtract process high-water marks.

Timing medians are independent; their components need not sum after aggregation.

string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 100.6 / 100.8 100.7 / 100.9 101.0 / 101.0 101.8 / 102.0
16 100.7 / 101.0 101.0 / 101.1 101.9 / 102.0 105.0 / 104.6
256 101.3 / 101.4 102.8 / 102.6 115.1 / 112.5 154.6 / 146.2
4,096 107.7 / 106.8 130.8 / 126.2 322.5 / 280.6 1,002.6 / 895.9

string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 101.0 / 100.8 101.0 / 101.1 101.2 / 101.1 101.6 / 101.8
16 101.2 / 101.6 101.6 / 101.7 103.5 / 103.5 112.0 / 111.7
256 101.9 / 101.9 104.0 / 103.8 116.8 / 114.5 163.6 / 158.1
4,096 109.7 / 109.7 136.7 / 131.2 337.0 / 305.0 1,029.0 / 830.6

large_string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 100.6 / 100.7 101.1 / 100.8 101.1 / 101.1 101.8 / 101.8
16 100.8 / 101.0 101.1 / 101.4 101.9 / 101.9 105.0 / 104.6
256 101.3 / 101.3 102.8 / 102.6 114.8 / 112.4 153.5 / 145.8
4,096 106.9 / 106.8 130.8 / 126.2 322.0 / 280.0 1,092.2 / 855.4

large_string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 100.7 / 100.9 100.8 / 100.8 100.9 / 101.1 101.7 / 101.9
16 101.3 / 101.3 101.7 / 101.9 103.5 / 103.2 112.2 / 112.0
256 102.0 / 102.2 104.3 / 104.2 117.1 / 114.4 167.3 / 157.8
4,096 110.0 / 109.1 135.1 / 130.4 324.5 / 305.3 1,000.4 / 923.4

List = array.to_pylist() + encode_batch(); Arrow = encode_batch_arrow(). Native import/validation belongs to Arrow encoding time. Full precision is retained in the saved CSV and JSON results.

Process peak memory before measurement (MiB)

Process peak memory before measurement (MiB)

Every cell: List / Arrow, median across five workers. Context diagnostic; no performance ranking.

Median high-water mark after fixture setup and full-batch warmup, before timed calls. Subtracting this from the later peak does not measure operation allocations.

Timing medians are independent; their components need not sum after aggregation.

string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 100.6 / 100.8 100.7 / 100.9 101.0 / 101.0 101.8 / 102.0
16 100.7 / 101.0 101.0 / 101.1 101.9 / 102.0 104.7 / 104.6
256 101.3 / 101.4 102.8 / 102.6 115.1 / 112.5 153.6 / 146.1
4,096 106.9 / 106.8 130.7 / 126.2 322.4 / 280.6 969.8 / 800.6

string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 101.0 / 100.8 101.0 / 101.1 101.2 / 101.1 101.6 / 101.8
16 101.2 / 101.6 101.6 / 101.7 103.1 / 103.5 108.5 / 107.9
256 101.9 / 101.7 103.6 / 103.3 116.8 / 114.5 159.9 / 151.7
4,096 108.2 / 108.5 133.3 / 128.9 324.9 / 282.6 994.5 / 825.2

large_string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 100.6 / 100.7 101.1 / 100.8 101.1 / 101.1 101.8 / 101.8
16 100.8 / 101.0 101.1 / 101.4 101.9 / 101.9 104.8 / 104.5
256 101.3 / 101.3 102.8 / 102.6 114.8 / 112.4 153.5 / 145.8
4,096 106.9 / 106.8 130.7 / 126.0 322.0 / 280.0 969.2 / 800.3

large_string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 100.7 / 100.9 100.8 / 100.8 100.9 / 101.1 101.7 / 101.9
16 101.3 / 101.3 101.7 / 101.9 103.3 / 103.0 108.8 / 107.8
256 101.8 / 102.0 104.0 / 103.4 117.1 / 114.4 160.0 / 151.4
4,096 108.1 / 108.5 133.2 / 129.0 324.5 / 282.8 996.4 / 825.7

List = array.to_pylist() + encode_batch(); Arrow = encode_batch_arrow(). Native import/validation belongs to Arrow encoding time. Full precision is retained in the saved CSV and JSON results.

Timed calls per worker (calls)

Timed calls per worker (calls)

Every cell: List / Arrow, median across five workers. Context diagnostic; no performance ranking.

Median completed calls across workers. The adaptive loop runs until its time target is reached.

Timing medians are independent; their components need not sum after aggregation.

string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 54,379 / 49,185 11,049 / 10,806 1,507 / 1,534 379 / 385
16 3,186 / 3,227 664 / 673 94 / 95 23 / 24
256 233 / 242 43 / 43 6 / 6 2 / 2
4,096 15 / 16 3 / 3 1 / 1 1 / 1

string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 53,126 / 47,395 10,524 / 10,291 1,454 / 1,472 372 / 373
16 3,739 / 4,194 1,429 / 1,509 280 / 284 76 / 79
256 415 / 449 119 / 128 18 / 18 5 / 5
4,096 27 / 30 8 / 8 2 / 2 1 / 1

large_string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 52,618 / 46,998 10,626 / 10,417 1,467 / 1,476 367 / 377
16 3,095 / 3,214 637 / 663 92 / 93 23 / 23
256 229 / 236 43 / 43 6 / 6 2 / 2
4,096 15 / 15 3 / 3 1 / 1 1 / 1

large_string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 52,747 / 46,953 10,583 / 10,408 1,464 / 1,473 367 / 374
16 3,779 / 4,324 1,446 / 1,517 279 / 284 76 / 78
256 408 / 445 119 / 126 18 / 19 5 / 5
4,096 27 / 30 8 / 8 2 / 2 1 / 1

List = array.to_pylist() + encode_batch(); Arrow = encode_batch_arrow(). Native import/validation belongs to Arrow encoding time. Full precision is retained in the saved CSV and JSON results.

Cumulative timed duration per worker (ms)

Cumulative timed duration per worker (ms)

Every cell: List / Arrow, median across five workers. Context diagnostic; no performance ranking.

Median sum of timed calls per worker, rather than per-call time or full worker wall time. At least 200 ms was requested; one long call can overshoot that target.

Timing medians are independent; their components need not sum after aggregation.

string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 200.002 / 200.001 200.009 / 200.011 200.082 / 200.062 200.291 / 200.103
16 200.046 / 200.019 200.17 / 200.132 201.045 / 201.299 202.324 / 204.429
256 200.484 / 200.407 202.045 / 200.588 207.439 / 205.366 280.757 / 274.071
4,096 206.192 / 208.041 227.961 / 223.724 566.048 / 555.702 2,279.5 / 2,270.64

string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 200.003 / 200.002 200.009 / 200.009 200.04 / 200.078 200.336 / 200.126
16 200.024 / 200.023 200.087 / 200.078 200.263 / 200.339 201.48 / 201.674
256 200.244 / 200.229 200.455 / 200.517 205.701 / 202.574 226.211 / 220.241
4,096 201.847 / 203.93 216.666 / 203.728 374.168 / 359.286 738.631 / 709.78

large_string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 200.002 / 200.002 200.007 / 200.005 200.04 / 200.046 200.398 / 200.282
16 200.031 / 200.045 200.147 / 200.226 201.402 / 201.445 203.446 / 201.41
256 200.67 / 200.457 203.497 / 203.4 210.75 / 207.243 279.595 / 278.253
4,096 212.303 / 205.262 230.663 / 230.949 554.882 / 549.485 2,252.35 / 2,259.61

large_string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 200.002 / 200.003 200.01 / 200.007 200.041 / 200.072 200.192 / 200.426
16 200.03 / 200.024 200.057 / 200.05 200.671 / 200.101 201.265 / 200.482
256 200.392 / 200.042 200.892 / 200.475 206.386 / 206.941 231.725 / 221.375
4,096 204.288 / 202.486 218.753 / 208.347 368.811 / 352.324 716.719 / 684.068

List = array.to_pylist() + encode_batch(); Arrow = encode_batch_arrow(). Native import/validation belongs to Arrow encoding time. Full precision is retained in the saved CSV and JSON results.

UTF-8 payload per batch (bytes)

UTF-8 payload per batch (bytes)

Every cell: List / Arrow, median across five workers. Context diagnostic; no performance ranking.

Actual input bytes per batch; expected to match between methods. Body-length column labels remain characters.

Timing medians are independent; their components need not sum after aggregation.

string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 37 / 37 302 / 302 2,438 / 2,438 9,767 / 9,767
16 592 / 592 4,832 / 4,832 39,008 / 39,008 156,272 / 156,272
256 9,472 / 9,472 77,312 / 77,312 624,128 / 624,128 2,500,352 / 2,500,352
4,096 151,552 / 151,552 1,236,992 / 1,236,992 9,986,048 / 9,986,048 40,005,632 / 40,005,632

string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 37 / 37 302 / 302 2,438 / 2,438 9,767 / 9,767
16 592 / 592 4,832 / 4,832 39,008 / 39,008 156,272 / 156,272
256 9,472 / 9,472 77,312 / 77,312 624,128 / 624,128 2,500,352 / 2,500,352
4,096 151,552 / 151,552 1,236,992 / 1,236,992 9,986,048 / 9,986,048 40,005,632 / 40,005,632

large_string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 37 / 37 302 / 302 2,438 / 2,438 9,767 / 9,767
16 592 / 592 4,832 / 4,832 39,008 / 39,008 156,272 / 156,272
256 9,472 / 9,472 77,312 / 77,312 624,128 / 624,128 2,500,352 / 2,500,352
4,096 151,552 / 151,552 1,236,992 / 1,236,992 9,986,048 / 9,986,048 40,005,632 / 40,005,632

large_string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 37 / 37 302 / 302 2,438 / 2,438 9,767 / 9,767
16 592 / 592 4,832 / 4,832 39,008 / 39,008 156,272 / 156,272
256 9,472 / 9,472 77,312 / 77,312 624,128 / 624,128 2,500,352 / 2,500,352
4,096 151,552 / 151,552 1,236,992 / 1,236,992 9,986,048 / 9,986,048 40,005,632 / 40,005,632

List = array.to_pylist() + encode_batch(); Arrow = encode_batch_arrow(). Native import/validation belongs to Arrow encoding time. Full precision is retained in the saved CSV and JSON results.

Output tokens per batch (tokens)

Output tokens per batch (tokens)

Every cell: List / Arrow, median across five workers. Context diagnostic; no performance ranking.

Returned tokens per batch; expected to match between methods, independent of worker iteration count.

Timing medians are independent; their components need not sum after aggregation.

string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 6 / 6 49 / 49 394 / 394 1,576 / 1,576
16 96 / 96 784 / 784 6,304 / 6,304 25,216 / 25,216
256 1,536 / 1,536 12,544 / 12,544 100,864 / 100,864 403,456 / 403,456
4,096 24,576 / 24,576 200,704 / 200,704 1,613,824 / 1,613,824 6,455,296 / 6,455,296

string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 6 / 6 49 / 49 394 / 394 1,576 / 1,576
16 96 / 96 784 / 784 6,304 / 6,304 25,216 / 25,216
256 1,536 / 1,536 12,544 / 12,544 100,864 / 100,864 403,456 / 403,456
4,096 24,576 / 24,576 200,704 / 200,704 1,613,824 / 1,613,824 6,455,296 / 6,455,296

large_string · 1 Rayon thread

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 6 / 6 49 / 49 394 / 394 1,576 / 1,576
16 96 / 96 784 / 784 6,304 / 6,304 25,216 / 25,216
256 1,536 / 1,536 12,544 / 12,544 100,864 / 100,864 403,456 / 403,456
4,096 24,576 / 24,576 200,704 / 200,704 1,613,824 / 1,613,824 6,455,296 / 6,455,296

large_string · 4 Rayon threads

Batch size 32 chars 256 chars 2,048 chars 8,192 chars
1 6 / 6 49 / 49 394 / 394 1,576 / 1,576
16 96 / 96 784 / 784 6,304 / 6,304 25,216 / 25,216
256 1,536 / 1,536 12,544 / 12,544 100,864 / 100,864 403,456 / 403,456
4,096 24,576 / 24,576 200,704 / 200,704 1,613,824 / 1,613,824 6,455,296 / 6,455,296

List = array.to_pylist() + encode_batch(); Arrow = encode_batch_arrow(). Native import/validation belongs to Arrow encoding time. Full precision is retained in the saved CSV and JSON results.

Both input paths, UTF-8 throughput, and variability

Values are medians with Q1–Q3 ranges across five fresh workers, or five paired ratios. Quartiles use inclusive linear interpolation. These ranges are descriptive, not confidence intervals; eight of the 64 paired ranges include 1x. All cells are retained, including slowdowns. MiB/s uses actual UTF-8 bytes divided by 1,048,576.

Arrow type Threads Batch Chars List docs/s [Q1–Q3] Arrow docs/s [Q1–Q3] List MiB/s Arrow MiB/s Paired speedup [Q1–Q3]
string 1 1 32 271,891 [268,169–274,627] 245,923 [245,160–246,620] 9.59 8.68 0.907 [0.893–0.917]x
string 1 1 256 55,244 [54,866–55,247] 54,027 [53,851–54,120] 15.91 15.56 0.980 [0.971–0.983]x
string 1 1 2,048 7,532 [7,517–7,537] 7,667 [7,628–7,699] 17.51 17.83 1.018 [1.015–1.022]x
string 1 1 8,192 1,892 [1,862–1,915] 1,924 [1,904–1,932] 17.62 17.92 1.005 [0.986–1.037]x
string 1 16 32 254,821 [252,201–255,047] 258,117 [256,399–261,935] 8.99 9.11 1.028 [1.017–1.030]x
string 1 16 256 53,075 [52,976–54,075] 53,835 [53,439–55,059] 15.29 15.51 1.014 [1.009–1.018]x
string 1 16 2,048 7,462 [7,397–7,481] 7,551 [7,346–7,563] 17.35 17.56 1.000 [0.993–1.012]x
string 1 16 8,192 1,838 [1,832–1,852] 1,878 [1,863–1,885] 17.12 17.50 1.014 [1.014–1.015]x
string 1 256 32 297,520 [294,599–298,883] 308,913 [308,406–310,055] 10.50 10.90 1.039 [1.028–1.049]x
string 1 256 256 54,534 [54,483–54,609] 54,960 [54,822–56,155] 15.71 15.83 1.008 [1.006–1.020]x
string 1 256 2,048 7,405 [7,385–7,470] 7,479 [7,457–7,549] 17.22 17.39 1.007 [0.995–1.013]x
string 1 256 8,192 1,824 [1,810–1,836] 1,868 [1,835–1,871] 16.99 17.40 1.024 [1.020–1.034]x
string 1 4,096 32 289,300 [284,472–295,308] 309,367 [303,136–309,724] 10.21 10.92 1.057 [1.048–1.066]x
string 1 4,096 256 53,904 [53,635–53,963] 54,925 [54,909–55,086] 15.52 15.82 1.018 [1.017–1.026]x
string 1 4,096 2,048 7,236 [7,220–7,252] 7,371 [7,292–7,456] 16.82 17.14 1.010 [1.009–1.019]x
string 1 4,096 8,192 1,797 [1,777–1,822] 1,804 [1,799–1,824] 16.74 16.80 1.004 [1.004–1.013]x
string 4 1 32 265,627 [262,121–265,916] 236,972 [236,632–237,100] 9.37 8.36 0.892 [0.890–0.896]x
string 4 1 256 52,618 [52,054–53,103] 51,453 [51,408–51,784] 15.15 14.82 0.969 [0.965–0.996]x
string 4 1 2,048 7,266 [7,253–7,335] 7,359 [7,302–7,402] 16.89 17.11 1.009 [1.005–1.031]x
string 4 1 8,192 1,859 [1,840–1,862] 1,864 [1,862–1,865] 17.32 17.36 1.003 [0.989–1.013]x
string 4 16 32 299,040 [292,852–299,706] 335,482 [324,189–336,104] 10.55 11.84 1.121 [1.107–1.153]x
string 4 16 256 114,270 [113,934–114,559] 120,667 [120,348–121,012] 32.91 34.75 1.053 [1.053–1.054]x
string 4 16 2,048 22,371 [21,328–22,399] 22,682 [22,411–23,109] 52.01 52.74 1.035 [1.033–1.036]x
string 4 16 8,192 6,035 [6,016–6,126] 6,268 [6,179–6,308] 56.22 58.38 1.048 [1.006–1.051]x
string 4 256 32 530,553 [526,417–542,351] 574,063 [558,860–578,247] 18.72 20.26 1.090 [1.058–1.116]x
string 4 256 256 151,974 [150,914–152,606] 163,230 [161,396–163,418] 43.77 47.01 1.082 [1.062–1.083]x
string 4 256 2,048 22,816 [22,401–22,873] 22,826 [22,747–23,913] 53.05 53.07 1.036 [1.015–1.048]x
string 4 256 8,192 5,658 [5,579–5,854] 5,812 [5,675–5,927] 52.71 54.13 1.008 [0.999–1.043]x
string 4 4,096 32 551,025 [535,904–568,192] 601,467 [590,038–602,148] 19.44 21.22 1.070 [1.060–1.101]x
string 4 4,096 256 151,237 [150,944–151,901] 160,842 [160,806–161,757] 43.56 46.32 1.059 [1.043–1.082]x
string 4 4,096 2,048 21,395 [21,376–21,894] 22,801 [22,773–23,115] 49.74 53.01 1.066 [1.056–1.112]x
string 4 4,096 8,192 5,545 [5,517–5,655] 5,771 [5,768–5,815] 51.65 53.75 1.046 [1.024–1.048]x
large_string 1 1 32 263,088 [261,111–263,198] 234,989 [234,280–235,736] 9.28 8.29 0.898 [0.896–0.900]x
large_string 1 1 256 53,130 [52,843–53,346] 52,083 [51,715–52,344] 15.30 15.00 0.980 [0.969–0.991]x
large_string 1 1 2,048 7,334 [7,214–7,401] 7,378 [7,165–7,379] 17.05 17.15 0.999 [0.996–1.003]x
large_string 1 1 8,192 1,831 [1,825–1,837] 1,881 [1,834–1,913] 17.06 17.52 1.009 [1.005–1.027]x
large_string 1 16 32 247,552 [241,403–249,853] 257,109 [253,287–260,021] 8.74 9.07 1.029 [1.026–1.037]x
large_string 1 16 256 50,894 [49,530–51,597] 53,030 [52,900–53,281] 14.66 15.27 1.033 [1.029–1.068]x
large_string 1 16 2,048 7,295 [7,261–7,309] 7,360 [7,327–7,387] 16.96 17.11 1.005 [1.003–1.014]x
large_string 1 16 8,192 1,824 [1,820–1,840] 1,840 [1,830–1,851] 16.99 17.14 1.009 [1.006–1.012]x
large_string 1 256 32 291,898 [285,763–292,650] 301,259 [300,877–301,391] 10.30 10.63 1.053 [1.026–1.054]x
large_string 1 256 256 53,885 [53,865–54,094] 54,586 [54,382–55,330] 15.52 15.72 1.017 [1.010–1.024]x
large_string 1 256 2,048 7,288 [7,285–7,341] 7,412 [7,373–7,460] 16.95 17.23 1.016 [1.012–1.020]x
large_string 1 256 8,192 1,831 [1,820–1,876] 1,840 [1,829–1,880] 17.06 17.14 1.006 [1.004–1.013]x
large_string 1 4,096 32 289,398 [289,361–289,405] 299,325 [298,997–299,835] 10.21 10.56 1.033 [1.033–1.045]x
large_string 1 4,096 256 53,273 [52,964–53,641] 53,207 [53,105–54,786] 15.34 15.32 1.003 [0.999–1.021]x
large_string 1 4,096 2,048 7,382 [7,358–7,384] 7,454 [7,438–7,480] 17.16 17.33 1.009 [1.007–1.013]x
large_string 1 4,096 8,192 1,819 [1,816–1,820] 1,813 [1,809–1,826] 16.94 16.88 0.998 [0.994–1.004]x
large_string 4 1 32 263,732 [261,558–266,003] 234,761 [231,811–237,031] 9.31 8.28 0.891 [0.884–0.897]x
large_string 4 1 256 52,912 [52,257–53,354] 52,037 [51,458–52,075] 15.24 14.99 0.976 [0.976–0.985]x
large_string 4 1 2,048 7,317 [7,261–7,329] 7,364 [7,317–7,414] 17.01 17.12 1.004 [1.001–1.019]x
large_string 4 1 8,192 1,833 [1,832–1,843] 1,866 [1,843–1,870] 17.07 17.38 1.006 [1.001–1.021]x
large_string 4 16 32 302,275 [299,081–312,084] 345,861 [331,892–349,479] 10.67 12.20 1.128 [1.110–1.144]x
large_string 4 16 256 115,666 [113,288–117,007] 121,348 [120,504–121,810] 33.31 34.95 1.054 [1.039–1.064]x
large_string 4 16 2,048 22,245 [20,936–22,687] 22,718 [22,716–23,234] 51.72 52.82 1.044 [1.041–1.065]x
large_string 4 16 8,192 6,023 [5,928–6,064] 6,225 [6,218–6,228] 56.10 57.98 1.034 [1.027–1.049]x
large_string 4 256 32 520,998 [519,940–521,428] 569,513 [561,802–574,186] 18.38 20.10 1.095 [1.078–1.129]x
large_string 4 256 256 152,290 [151,644–152,429] 160,898 [159,982–161,017] 43.86 46.34 1.056 [1.047–1.061]x
large_string 4 256 2,048 22,465 [22,311–22,528] 23,504 [23,343–23,631] 52.23 54.65 1.046 [1.035–1.065]x
large_string 4 256 8,192 5,524 [5,483–5,629] 5,782 [5,729–6,022] 51.45 53.86 1.047 [1.018–1.049]x
large_string 4 4,096 32 541,354 [532,843–549,427] 597,690 [586,628–607,081] 19.10 21.09 1.104 [1.033–1.154]x
large_string 4 4,096 256 149,794 [147,744–150,107] 158,363 [157,276–162,323] 43.14 45.61 1.051 [1.048–1.088]x
large_string 4 4,096 2,048 22,212 [22,069–22,293] 23,251 [23,122–23,473] 51.64 54.06 1.053 [1.037–1.054]x
large_string 4 4,096 8,192 5,715 [5,613–5,755] 5,988 [5,979–6,061] 53.23 55.77 1.061 [1.032–1.070]x

Reproduce from the repository root after installing PyArrow and a release build (maturin develop --release from bindings/python):

for threads in 1 4; do
  python bindings/python/benches/benchmark_arrow.py \
    --batch-sizes 1 16 256 4096 \
    --text-lengths 32 256 2048 8192 \
    --arrow-types string large_string \
    --threads "$threads" --repeats 5 --min-duration 0.2 \
    --format json > "benchmark-arrow-t${threads}.json"
done

--format markdown emits all metric matrices directly. Select a subset with, for example, --metrics pylist_ms encode_ms total_ms peak_rss_mib. JSON retains conversion/encoding time, documents/s, UTF-8 MiB/s, output tokens/s, iteration counts, individual samples and environment details. Process peak RSS includes imports, fixtures and warmup; it is not an allocation measurement for the encoding call alone.

Validation on macOS ARM64:

  • Python 3.13: full suite, 354 passed, 3 skipped; 10 deprecation warnings from existing fixture and fork tests.
  • Python 3.10: 151 Arrow tests passed using the ABI3 wheel.
  • Python 3.14t: 151 Arrow tests and 4 existing threading tests passed against a separate release build, with the GIL disabled throughout.
  • Rust binding tests: 20 passed; cargo fmt --check and cargo clippy --all-targets --all-features -- -D warnings passed.
  • Repository make check-style passed in a temporary source copy. The final Arrow method matches generated output, including Literal and its import. Ruff and ty checks passed; caller checks accept all three choices and reject an invalid value.
  • Strict Sphinx builds passed for Rust, Python, and Node; Python documentation was rebuilt against the final wheel without warnings.
  • cargo audit -D warnings passed with the repository's two existing advisory exclusions.
  • Wheel and source distribution built successfully; final source files are present in the source distribution. Import and ordinary encoding work in an environment without PyArrow.
  • Full throughput matrix completed: 64 configurations and 640 fresh worker samples. Rate arithmetic, paired workloads, minimum durations and reported medians were validated. Text, JSON and Markdown output smoke checks and Ruff checks passed.

The full regression and interpreter compatibility checks preceded the final typing-only update. After that update, the wheel was rebuilt and all 151 Arrow tests, stub generation, Clippy, and static typing checks passed again. Linux, Windows, and the remaining wheel targets still require upstream CI.

Related: #1415.

Accept Arrow string and large_string arrays through the C Data Interface without materializing Python strings. Preserve character offsets and support configurable null handling with Literal typing.

Add FFI lifetime and parity tests plus a Python benchmark matrix for conversion, encoding, throughput, and process peak memory.
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