feat(dataset_tools): prototype mutable (resizable) steps (#1483) - #1601
Draft
NJManganelli wants to merge 14 commits into
Draft
feat(dataset_tools): prototype mutable (resizable) steps (#1483)#1601NJManganelli wants to merge 14 commits into
NJManganelli wants to merge 14 commits into
Conversation
…etection
- get_steps now auto-detects TTree vs RNTuple per file: RNTuple num_entries,
cluster boundaries (from cluster_summaries), and form (via uproot.dask with the
{path: object_path} spec, since the opened RNTuple object is not accepted) are
handled alongside the existing TTree path. preprocess()/preprocess_root handle
RNTuples transparently.
- Add preprocess_rntuple(), mirroring preprocess_parquet/preprocess_root, which
requires every object to be an RNTuple (require_rntuple=True) and raises
otherwise; exported from coffea.dataset_tools.
- Extract shared helpers _even_steps, _aligned_steps, _rntuple_cluster_boundaries,
and _union_form_jsonstr; use them in get_steps and get_parquet_form_uuid_steps
and de-duplicate the union-form logic across the legacy and pydantic paths.
- Short-circuit an empty per-format DataGroupSpec in _preprocess_pydantic so a
single-format fileset no longer issues a wasted (empty) dask.compute.
- Remove the spurious dask function-cache pop in the parquet path (no graph is
built there) and guard the TTree/RNTuple pop against an empty cache; drop the
always-true 'num_entries >= 0' guard; correct file_exceptions/parquet docstrings.
- Add end-to-end test_preprocess_rntuple and a filter_files->PreprocessedFiles test.
Assisted-by: Claude Opus 4.8
…ree forms Factor the per-dataset preprocessing map-reduce out of _preprocess_pydantic behind a small backend interface (new module dataset_tools/backends.py) shaped to align with the coffea.compute refactor (PR scikit-hep#1470): PreprocessJob (computable) -> Backend.submit() -> future-like Task.result(), with an ordered concat reduce (preprocessing zips results against original file order). Backends: - DaskBackend: the historical from_awkward/map_partitions/AwkwardTreeReductionLayer graph; the only backend that imports dask for orchestration. - IterativeBackend: immediate (synchronous, single process), mirroring IterativeExecutor. Dask-free. - FuturesBackend: concurrent.futures pool, threads by default with a use_processes opt-in. Dask-free. preprocess()/preprocess_root/_rntuple/_parquet gain backend= (default "dask", so existing behavior is unchanged). On a dask import failure the default path prints a coffea_console hint pointing at the dask-free backends. The Runner is not rewired; that is left to the compute refactor. TTree form extraction is now dask-free: it uses uproot's own non-dask form builder (uproot._dask._get_ttree_form, guarded with a fallback), byte-identical to uproot.dask(tree).layout.form, which also drops the function_cache workaround for TTrees. RNTuple form extraction still uses uproot.dask (documented). Also fixes found while self-reviewing this change: - get_parquet_form_uuid_steps: add the num_entries==0 guard ROOT already had, so a 0-row parquet yields steps=[[0,0]] instead of ZeroDivisionError. - Scope the dask fallback hint to submit() import failure so a worker's own ModuleNotFoundError during compute is no longer misreported as "dask missing". - FuturesBackend: shut down an owned pool via __del__ if the task is only wait()-ed or dropped without result(). - resolve_backend: warn when scheduler is passed with a pre-built backend instance (previously dropped silently for a DaskBackend instance). - DaskBackend: clamp files_per_batch with max(1, ...) for parity with the other backends. Tests: tests/test_dataset_tools_backends.py; dask/iterative/futures verified to produce equal DataGroupSpec, and the non-dask TTree form verified byte-identical to the dask form. Assisted-by: Claude Opus 4.8
An explicit step_size=0 (or negative) previously surfaced as a bare ZeroDivisionError from _even_steps deep inside a worker/dask graph. Add a _validate_step_size guard, called at the start of _preprocess_pydantic (covers preprocess/preprocess_root/_rntuple/_parquet) and preprocess_legacy, raising a clear ValueError before any file I/O. None and >= 1 are unaffected. Assisted-by: Claude Opus 4.8
Extend the dask-free form path from TTree to RNTuple by routing both through uproot's own form builder (uproot._dask._get_ttree_form, which already handles RNTuple HasFields sub-branches) and nulling the resulting form keys. The RNTuple delay-open dask path builds base_form the same way; the only delta was that uproot.dask exposes the keyless meta form while _get_ttree_form carries RNTuple "column-N" keys, so _null_form_keys closes the gap (a no-op for already-keyless TTree forms). _ttree_form_json is replaced by the format-aware _awkward_form_json (filter_field + full_paths for RNTuple, filter_branch for TTree). uproot.dask is now only a fallback for the unlikely case that the private uproot helper is unavailable. Net result: iterative/futures backends are fully dask-free for parquet and both ROOT flavors. Verified byte-identical to uproot.dask(...).layout.form for TTree and RNTuple samples (parametrized test), and dask/iterative/futures produce equal DataGroupSpec end-to-end for an RNTuple dataset. Assisted-by: Claude Opus 4.8
… rename backends module Pre-pay the coffea.compute (PR scikit-hep#1470) migration on two axes: (a) Order-relaxation. _preprocess_pydantic previously zipped the concatenated per-file results positionally against the original input to recover skipped/ bad files (emitted as None by the worker), which made the reduce order load-bearing. Assemble both `available` and `updated` filesets by filename instead, in the original input order, so a skipped file is identified by its absence from the results rather than by position. Correctness no longer depends on the order in which a backend reduces/concatenates, which is what lets preprocessing mesh with an order-agnostic compute reduce. The shared get_steps worker (also used by the legacy path) is untouched. (b) Task.partial_result(). Add partial_result() to the PreprocessTask protocol and all three tasks, mirroring compute.Task: the eager task returns its full result, the futures task gathers only completed batches without blocking (and without shutting the pool), and the dask task falls back to the full result (a fused graph has no cheap partial). Also rename backends.py -> preprocess_backends.py (clearer alongside a future coffea.compute.backends) and soften the now-obsolete "ordering is load-bearing" docs. Tests: filename-based assembly of a skipped bad file; partial_result == result when complete. Full preprocess suite + backend suite green; pre-commit clean. Assisted-by: Claude Opus 4.8
…ocessing filespec.ipynb: extend the "Integration with Preprocessing" section with three new subsections (executed against the on-master samples, real outputs attached): - 5.2 Switchable execution backends (backend="dask"/"iterative"/"futures", FuturesBackend instances), showing the dask-free backends agree with dask. - 5.3 Preprocessing Parquet datasets (preprocess_parquet, format auto-detection, no object_path, use_row_groups). - 5.4 Preprocessing RNTuple datasets (auto-detection; preprocess_rntuple's RNTuple-only contract). Imports and the overview are updated accordingly; later cells' execution counts are renumbered to stay monotonic. The remaining cells' committed outputs are untouched (the apply_to_fileset/dask-histogram compute path can't be re-run in the current env due to a known awkward/dask-histogram version-compat baseline, unrelated to preprocessing). processing.ipynb: add a short note to the Preprocessing section pointing at the new backend= argument and Parquet/RNTuple support, linking to filespec.ipynb. (docs/source/notebooks/*.ipynb are symlinks into binder/, so they update too.) Assisted-by: Claude Opus 4.8
Re-execute the entire notebook end-to-end (nbconvert --execute) now that the environment is unblocked: apply_to_fileset -> dask.compute runs cleanly on the advanced scikit-hep stack (uproot 5.7.4, awkward 2.10.0, dask-awkward 2026.2.1, dask-histogram 2026.2.0), which previously crashed with the awkward/dask-awkward scalar-meta incompatibility. Every cell (including the Integration-with- apply_to_fileset section and the 5.2/5.3/5.4 backends/parquet/rntuple demos) now carries real, consistently-numbered outputs from a single fresh run -- replacing the earlier partial-output render. 0 error outputs. Assisted-by: Claude Opus 4.8
…bsent test_awkward_form_json_matches_uproot_dask compares the dask-free form builder against uproot.dask output, so it requires dask and dask-awkward; guard it with pytest.importorskip like the other dask-dependent tests in this module so the no-dask CI job skips it. Assisted-by: Claude Fable 5
- require_rntuple raises inside the per-file error handling so skip_bad_files/file_exceptions can skip TTree files, and rejects parquet-format datasets with a clear error - preprocess_root gains require_rntuple=; preprocess_rntuple becomes a thin alias instead of a duplicated 13-parameter signature - get_steps/get_parquet_form_uuid_steps validate step_size directly (negative values previously produced a silent single step per file) - drop the unreachable duplicate zero-row guard in get_parquet_form_uuid_steps - forward uproot_options to the RNTuple uproot.dask form fallback - futures backend: fail fast on batch failure (cancel pending batches), and default pool sizing to the executor default instead of 1 worker - resolve_backend injects scheduler into a DaskBackend instance that has none; preprocess resolves the backend once so mixed filesets do not double-warn - serialize the null-key form dict directly (skips a Form round-trip) and reuse a single to_list in the by-filename assembly - comment-convention cleanups (no PR/review references, present-tense descriptions) Assisted-by: Claude Fable 5
…field bitsets Adding two DatasetSpecs that both carry saved forms now computes the union form (fields appearing in either operand) instead of discarding it, so separately preprocessed specs combine without re-opening files. Adding a form-bearing spec to a form-less one raises, since the union could not describe the form-less files. - new dataset_tools/forms.py hosts the union helper (moved from preprocess.py) plus sort_form_fields, prune_form_fields, and hex field-bitset encode/decode - DatasetSpec.union_with(other, sort_fields=...) is the explicit form; sort_fields=True canonicalizes the serialized field order for byte-stable output independent of operand order (form equality is field-order-insensitive either way) - DatasetSpec.canonicalize_form() re-serializes the saved form with recursively sorted fields and remaps bitsets - experimental: file specs carry experimental_field_bitset, a hex bitset over the dataset union form's top-level fields, populated during pydantic preprocessing; filter_files/limit_files prune the union form to the surviving files' fields when every remaining file has a bitset - experimental fields are excluded from file-spec equality and from the legacy dict conversions Assisted-by: Claude Fable 5
…parquet) Add two real CMS NanoAOD files (and their parquet conversions) carrying disjoint subsets of GenModel_TChiZH_* model-point flags -- file A has GenModel_TChiZH_700_1, file B does not. Adding the two separately preprocessed DatasetSpecs unions their forms into a superset in which a flag present in only one file becomes an IndexedOptionArray(bool), so it stays readable (as None) for the file that lacks it. This is the GenModel case scikit-hep#1478 targets and exercises the option-type union path for both ROOT and parquet, alongside the existing synthetic HLT unit tests. Assisted-by: Claude Fable 5
This was referenced Jul 20, 2026
Add materialization regressions over the union read path (dask mode, where the union form injects an option-typed flag physically absent from a file). Reading file A through the dataset union form yields the same GenModel_TChiZH_700_1 selection as reading A through its own form (20), so events passing a model-point mask are not falsified when the flag is made optional by unioning with a file that lacks it. For each model point the combined A+B count equals the per-file sum and the per-file truth (700_1: 20 from A; 950_400: 20 from B; 1100_200: 0). A second test pins that selecting on the raw option-type mask keeps the injected None rows and over-selects (40), so fill_none(flag, False) is required (20). Assisted-by: Claude Fable 5
…essing
preprocess/preprocess_root/preprocess_rntuple/preprocess_parquet accept
metadata_extractor= and metadata_reducer= on the pydantic path:
- metadata_extractor(file_handle) runs once per file inside the worker
on the already-open handle (uproot ReadOnlyDirectory for ROOT, the
awkward.metadata_from_parquet mapping for parquet) and returns a
JSON-serializable dict, stored as that file spec's new metadata field.
It runs inside the per-file error handling, so extraction failures
participate in skip_bad_files/file_exceptions; results travel through
the map-reduce as a JSON string so heterogeneous per-file dicts
concatenate cleanly on every backend.
- metadata_reducer({filename: dict}) runs once per dataset over the
available files' extracted metadata and its dict result merges into
the dataset-level metadata on both returned filesets (e.g. summing
per-file CutBookkeepers sums-of-weights into a dataset total, the
ATLAS pattern from the IRIS-HEP integration challenge / issue design
discussion).
- file specs gain an optional metadata field (merged on file addition
with right precedence); legacy dict conversions drop per-file
metadata while the reduced dataset-level metadata survives dict
output. The legacy preprocessing path rejects the new arguments.
Assisted-by: Claude Fable 5
…izing New dataset_tools/mutable_steps.py: a send-channel generator protocol for step iteration where the consumer renegotiates the step size mid-stream (the shape a resource-monitoring scheduler needs to shrink chunks on worker memory pressure, and the same channel Computable.gen_steps uses in the compute protocol): - resizable_steps(start, stop, size): yields [begin, end] pairs; a sent size applies from the next step, at most the requested size, with the remainder re-tiled as evenly as possible (n = ceil(remaining/size); actual = ceil(remaining/n)) - iter_file_steps / iter_dataset_steps: tile a file spec's covered regions (merging adjacent stored steps) and chain across a dataset's files, with resize requests carrying across region and file boundaries - remaining_regions / completed_spec: interval-level resumption helpers for completed ranges that do not align with stored steps - WallTimeStepPolicy + run_adaptive_steps: toy driver that measures each step's wall time (injectable clock) and resizes toward a target seconds-per-step, with damped growth and min/max clamps Prototype module; not re-exported from coffea.dataset_tools. Assisted-by: Claude Fable 5
NJManganelli
force-pushed
the
feat/mutable-steps-prototype
branch
2 times, most recently
from
July 25, 2026 16:45
2416ee6 to
241e551
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 AI text below 🤖
Note
Targets
scikit-hep:master; builds on #1579, the union-form PR, and the metadata PR. Until those merge, this PR's diff also includes their commits; it collapses to just the mutable-steps changes once they land. Review after them.What this adds
New
coffea/dataset_tools/mutable_steps.py: asend-channel generator protocol for step iteration where the consumer renegotiates the step size mid-stream. This is the shape a resource-monitoring scheduler needs to shrink chunks under worker memory pressure, and it matches theComputable.gen_stepssend-channel in the #1470 compute protocol, so consumers written against it translate directly.resizable_steps(start, stop, size)yields[begin, end]pairs; a sent size applies from the next step, is at most the requested size (a resize triggered by resource exhaustion can never grow a step), and re-tiles the remainder evenly (n = ceil(remaining/size),actual = ceil(remaining/n)) — the semantics @btovar proposed in Dynamic chunking in coffea.compute #1483.iter_file_steps/iter_dataset_stepstile a file spec's covered regions (merging adjacent stored steps) and chain across a dataset's files, with resize requests carrying across region and file boundaries.remaining_regions/completed_specare interval-level resumption helpers for completed ranges that need not align with the stored steps (resized steps generally don't);completed_specresults accumulate via the ordinary step arithmetic.WallTimeStepPolicy+run_adaptive_stepsare a toy single-consumer driver that measures each step's wall time (injectable clock, so the control loop is testable without real waiting) and resizes toward a target seconds-per-step, with damped growth and min/max clamps.Prototype module — not re-exported from
coffea.dataset_tools; no executor is wired to it. The intent is to prototype the protocol shape so a resource-awarecoffea.compute/ TaskVine backend can adopt it.Try it (against the bundled test files)
Tests
tests/test_dataset_tools_mutable_steps.pycovers even tiling, shrink/grow-at-most-requested semantics, invalid sizes, region merging and disjoint-region iteration, resize carrying across files, interval subtraction / completed-spec accumulation, and the adaptive driver (convergence to target, damped growth with clamps).Developed with Claude Fable 5. Prototype — API may change; aligns with the #1470
gen_stepssend-channel.