Skip to content

iter(batch_size=…) yields PythonCall wrappers instead of "julia"-format batches #105

Closed

Description

@CarloLucibello

In the default "julia" format, rows (for row in itds) and slices (ds[1:2]) are converted with py2jl. The batched iterator ds.iter(; batch_size), the only way to stream an IterableDataset in batches, is forwarded to Python instead. Its batches come back in PythonCall's default conversion: a PyIterable of PyDicts holding PyArrays, with string columns as PyArray{Py} of Python str.

using HuggingFaceDatasets   # v0.4.3
ds = Dataset((; text=["a", "bb", "ccc"], n=Int32[1, 2, 3]))
b = first(ds.iter(; batch_size=2))
typeof(b)          # PythonCall.PyDict{Any, Any}
typeof(b["text"])  # PythonCall.PyArray{PythonCall.Py, 1, true, true, PythonCall.Wrap.UnsafePyObject}
typeof(ds[1:2])    # Dict{String, Vector}
first(ds.to_iterable_dataset().iter(; batch_size=2))  # the same PyDict

For streaming, batches are the fast path. On a local 400k-document TinyStories parquet, rows iterate at 26k rows/s in the "julia" format, while 10k-row batches converted with py2jl reach ~105k rows/s. Converting text batches is cheap: py2jl adds ~0.9 s per 100k FineWeb-Edu documents (482 MB of text). So with converted batches, a tokenization pipeline can work on Vector{String} batches and stay within 1–5% of keeping the text in Python (measured with the tokenizers GPT-2 tokenizer).

A possible fix: route iter through _method_override for Dataset and IterableDataset, so that it returns a Julia iterator applying the dataset's jltransform to each batch, e.g. Iterators.map(jltransform, py.iter(; kws...)). The current workaround is to call py2jl(b) on each batch.

Activity

  1. CarloLucibello commented on Oct 19, 2026

    @CarloLucibello
    CollaboratorAuthor

    Some data from the Flux GPT-2 example (FluxML/Flux.jl#2754), which keeps the text in Python today (data.jl: with_format(ds, nothing), then ds.py.select_columns("text").iter(; batch_size)):

    • The workaround in the description doesn't convert anything: ds.iter(...) yields PyDicts, and py2jl(::PyDict) returns its argument. What works now is py2jl(b) for b in ds.py.iter(; batch_size). py2jl could unwrap PythonCall's wrapper types (PyDict, PyList, PyArray, ...) through Py(x).
    • Julia strings cost nothing on the tokenizer side: tokenizers' encode_batch_fast accepts a Vector{String} as is, in 1.07 s against 1.11 s for the Python list (10k FineWeb-Edu documents, 10.3M tokens). End to end, streaming and tokenizing 300k FineWeb-Edu documents runs at 8.5 M tok/s with py2jl batches, and 8.6–9.3 M tok/s with raw Python batches.
    • select_columns is forwarded and keeps the "julia" format, so with this the example's text stream becomes ds.select_columns("text").iter(; batch_size).

    Releasing each batch's Python memory is #107.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions