Summary
Revisit the format / transform interface (and its implementation) now that
datasets >= 4 returns lazy Column objects for column access. The current
behavior is intentional and documented — this issue is a placeholder to
reconsider the design, not a bug report.
Current model
A Dataset has two independent underlying slots:
- the python format
pyds._format_type (set via datasets' own
set_format): nothing, "numpy", "torch", "pandas", "arrow",
"polars", …
- the julia
jltransform field: py2jl, identity, or a user function.
The public API couples them into a single "format" slot (src/dataset.jl,
set_format!):
"julia" ≡ (_format_type = nothing, jltransform = py2jl)
- any python format
X ≡ (_format_type = X, jltransform = identity)
So the two slots are mutually exclusive through the API: setting "julia"
resets the python format to nothing; setting a python format installs
identity. The one combination that cannot be expressed is "python format X
and py2jl on top."
What prompted this
With datasets >= 4, ds["col"] returns a lazy datasets.Column, which we now
wrap in a lazy HuggingFaceDatasets.Column <: AbstractVector (added alongside
the datasets >= 4 support). It was observed that the "julia" format calls reset_format(),
so ds.pyds["col"] is always a Column.
Worth recording: resetting to nothing is not what forces the Column. In
datasets 4, Dataset.__getitem__(str) returns a Column for every format
except arrow/pandas/polars:
| python format |
ds["col"] returns |
nothing (our "julia") |
datasets.Column (lazy) |
"numpy" |
datasets.Column (lazy) |
"torch" |
datasets.Column (lazy) |
"pandas" |
pandas.Series (native) |
"arrow" |
pyarrow.ChunkedArray (native) |
"polars" |
polars.Series (native) |
So decoupling the two formats would not avoid Columns in general — the lazy
Column is simply the v4 default for column access.
Points to reconsider
-
One format slot vs. two independent axes. Conceptually the python format
and the julia conversion are orthogonal (what datasets produces vs. a
julia-side transform). Exposing both is more faithful but roughly doubles the
state space; most combinations are redundant (py2jl already absorbs numpy
copylessly via DLPack) or ill-defined (py2jl has no branch for a pandas
Series / arrow table / torch tensor). Current stance: keep the single slot;
with_jltransform / MLUtils.mapobs are the intended composition points.
-
The one real limitation — numeric throughput. Under nothing, a numeric
batch comes back as a python list and py2jl copies it element-by-element;
under "numpy" it would be an np.ndarray → numpy2jl (zero-copy). This is
the single case where "numpy" + py2jl genuinely wins.
-
Possible internal optimization (benchmark first). Rather than exposing a
second format knob, consider whether the "julia" format should sit on
"numpy" instead of nothing, so numeric columns transfer copyless. Tradeoff:
image columns currently arrive as PIL.Image (→ Gray/RGB ImageCore
views) under nothing; under "numpy" they'd be raw transposed arrays and we
lose that clean path. Best validated with the perf/ harness on both numeric
and image datasets before changing anything.
Non-goals / decision so far
- Keep the current format behavior as-is for now (intentional, documented in the
guide's "The "julia" format and transforms" section).
- No API change until the numeric zero-copy question (2/3) is benchmarked and we
decide whether the win justifies either an internal base-format change or a
second, opt-in axis.
Summary
Revisit the format / transform interface (and its implementation) now that
datasets >= 4returns lazyColumnobjects for column access. The currentbehavior is intentional and documented — this issue is a placeholder to
reconsider the design, not a bug report.
Current model
A
Datasethas two independent underlying slots:pyds._format_type(set viadatasets' ownset_format):nothing,"numpy","torch","pandas","arrow","polars", …jltransformfield:py2jl,identity, or a user function.The public API couples them into a single "format" slot (
src/dataset.jl,set_format!):"julia"≡(_format_type = nothing, jltransform = py2jl)X≡(_format_type = X, jltransform = identity)So the two slots are mutually exclusive through the API: setting
"julia"resets the python format to
nothing; setting a python format installsidentity. The one combination that cannot be expressed is "python formatXand
py2jlon top."What prompted this
With
datasets >= 4,ds["col"]returns a lazydatasets.Column, which we nowwrap in a lazy
HuggingFaceDatasets.Column <: AbstractVector(added alongsidethe
datasets >= 4support). It was observed that the"julia"format callsreset_format(),so
ds.pyds["col"]is always aColumn.Worth recording: resetting to
nothingis not what forces theColumn. Indatasets4,Dataset.__getitem__(str)returns aColumnfor every formatexcept
arrow/pandas/polars:ds["col"]returnsnothing(our"julia")datasets.Column(lazy)"numpy"datasets.Column(lazy)"torch"datasets.Column(lazy)"pandas"pandas.Series(native)"arrow"pyarrow.ChunkedArray(native)"polars"polars.Series(native)So decoupling the two formats would not avoid
Columns in general — the lazyColumnis simply the v4 default for column access.Points to reconsider
One format slot vs. two independent axes. Conceptually the python format
and the julia conversion are orthogonal (what
datasetsproduces vs. ajulia-side transform). Exposing both is more faithful but roughly doubles the
state space; most combinations are redundant (
py2jlalready absorbs numpycopylessly via DLPack) or ill-defined (
py2jlhas no branch for a pandasSeries/ arrow table / torch tensor). Current stance: keep the single slot;with_jltransform/MLUtils.mapobsare the intended composition points.The one real limitation — numeric throughput. Under
nothing, a numericbatch comes back as a python
listandpy2jlcopies it element-by-element;under
"numpy"it would be annp.ndarray→numpy2jl(zero-copy). This isthe single case where
"numpy" + py2jlgenuinely wins.Possible internal optimization (benchmark first). Rather than exposing a
second format knob, consider whether the
"julia"format should sit on"numpy"instead ofnothing, so numeric columns transfer copyless. Tradeoff:image columns currently arrive as
PIL.Image(→Gray/RGBImageCoreviews) under
nothing; under"numpy"they'd be raw transposed arrays and welose that clean path. Best validated with the
perf/harness on both numericand image datasets before changing anything.
Non-goals / decision so far
guide's "The
"julia"format and transforms" section).decide whether the win justifies either an internal base-format change or a
second, opt-in axis.