Skip to content

feat: complete CPU kernel migration from parents to offsets#4056

Merged
ianna merged 41 commits into
mainfrom
awkward3
Jun 30, 2026
Merged

feat: complete CPU kernel migration from parents to offsets#4056
ianna merged 41 commits into
mainfrom
awkward3

Conversation

@ianna

@ianna ianna commented May 16, 2026

Copy link
Copy Markdown
Member

(Back-port from the awkward3 branch.)

What this changes

Reducer, sort, and unique machinery now passes contiguous offsets ranges to kernels instead of per-element parents labels.

The offsets contract. Every kernel that previously received parents now receives offsets of length outlength + 1 satisfying: offsets[0] == 0, offsets[outlength] == total number of rows covered, and monotone non-decreasing (empty bins allowed). Bin k is the contiguous range [offsets[k], offsets[k+1]).

  • ~100 CPU kernels rewritten in awkward-cpp against the new contract; kernel-specification.yml and kernel-test-data.json migrated to match.
  • All 14 content classes re-threaded with the new _reduce_next / _sort_next / _unique signatures (parents parameter removed).
  • ak.index.EmptyIndex / ak.index.ZeroIndex removed, and ak.index.LazyIndex (now unused) along with them (note: these were importable in released 2.x — low but nonzero third-party risk; see "Behavior changes").
  • CUDA backend moved largely onto cuda.compute (CCCL Python); see "Decisions for maintainers to confirm" below.
  • IndexedArray._unique simplified to carry-and-delegate, replacing ~120 lines of parents-era logic. Now covered by a regression test (tests/test_4056_indexedarray_unique.py) asserting the delegated result matches the materialized (project()) array across reordering, repeats, both axes, several dtypes, the empty fast-path, and a nested-list case — maintainer sign-off still welcome.

Threading. This revision keeps the rewritten kernels serial; it does not include the OpenMP parallelization prototyped earlier. As a result, published manylinux wheels remain single-threaded and the auditwheel-bundled-libgomp / dask-oversubscription consideration does not apply at this head. It would return if OpenMP is reintroduced, and should be a deliberate decision at that point.

Behavior changes

  • Floating-point reductions may differ in the last few ULPs from earlier releases. The rewrite changes the order in which floating-point values are combined; since FP add/multiply are not associative, ak.sum / ak.prod (and quantities derived from them) on float32/float64/complex data can differ at the bit level from previous releases. Results stay correct to within normal rounding — ak.isclose / numpy.isclose continue to pass — but exact comparisons (==, ak.array_equal without a tolerance) may now report a difference. min, max, argmin, argmax, count, and countnonzero are unaffected (order-independent or exact). Code relying on bit-for-bit reproducibility against an older Awkward should compare with a tolerance.
  • Removed undocumented internal index classes ak.index.EmptyIndex, ak.index.ZeroIndex, and ak.index.LazyIndex. Third-party code importing them directly will raise ImportError. No supported, documented path used them.

Decisions for maintainers to confirm

  • 21 rewritten CUDA reducer kernels are currently disabled (commented out of cuda_kernels_impl in dev/generate-kernel-signatures.py), so reducer dispatch on CUDA always uses cuda.compute. The CUDA backend already relies on an undeclared, user-supplied prerequisite — CuPy is not declared in pyproject.toml either — so this follows existing precedent rather than introducing a new pattern. What changes is that the required-but-undeclared set grows from {CuPy} to {CuPy, CCCL}, now covering reductions and ~70 structural kernels, with no CuPy-only fallback if CCCL is absent. CCCL is documented for CI in requirements-test-gpu.txt (cuda-cccl>=1.0.2). Whether to declare it (e.g. as a GPU extra), re-enable the hand-written kernels, or delete them is left open here, pending GPU validation.
  • Six parents-era kernels (awkward_sorting_ranges, awkward_reduce_local_nextparents_64, …) remain in the spec with no live CPU caller — kept for now as deprecated; can be dropped in a follow-up.

Release management

  • Bump awkward-cpp 53 → 54 in awkward-cpp/pyproject.toml and the matching pin in pyproject.toml (both still read 53). The C++ kernel signatures changed, so an awkward-cpp 54 must be published before releasing this awkward; otherwise awkward_cpp==53 pulls the old kernels against the new Python and breaks on install.

Testing

  • Full main suite, tests-spec, tests-cpu-kernels pass; migrated kernel-test-data.json entries validated (offsets well-formed, empty bins → identity, argmin/argmax absolute-index conventions).
  • New hand-written tests:
    • tests/test_4056_record_reducer_shifts.py — positional record reducers with missing values inside lists, incl. typetracer.
    • tests/test_4056_indexedarray_unique.pyIndexedArray._unique parity with project() (regression cover for the carry-and-delegate rewrite).
  • New unit-test data for awkward_RegularArray_reduce_nonlocal_preparenext_64 (7 cases covering uneven and empty bins) generates coverage on CPU and CUDA backends.
  • The CPU awkward_sort kernel guards the empty-offsets case (offsetslength == 0) and clamps bin bounds against length, closing a potential out-of-bounds read on malformed offsets.
  • CUDA suites (tests-cuda, tests-cuda-kernels*) still need a run on real hardware before merge.
  • tests-cuda/test_3149...test_block_boundary_prod_complex13 fixes a pre-existing out-of-bounds read.

AI disclosure

Parts of this PR were developed with AI assistance (per CONTRIBUTING.md).

* next step in kernel migration from parents to offsets

* linter fix

* add jax bincount

* add cpp kernel to convert parents to offsets

* fix typo

* fix typo in an auto-generated test

* almost there

* nearly there

* cleanup

* fix remaining kernels

* final cleanup

* format json data

* format

* initialize maxnextparents  = -1 (a sentinel meaning "no bin was touched")

* update the kernels to work on offsets!

* format

* migrate cupy rawkernels from parents to offsets

* migrate jax reducers

* fix for platfroms where the int64 counts can't be safely cast

* add bincount for cupy backend

* compact loc

* fix windows build and add optional OpenMP support

* update cuda kernels

* feat: add `missing_repeat` kernel implementation using cuda.compute (#3922)

* feat: add missing_repeat implementation using cuda.compute

* keep the same name as in cpu kernel

* add tests for repetitions>1 and regularsize>1

* style: pre-commit fixes

* add keywords

* style fixes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* feat: add `index_rpad_and_clip` kernels implementations using cuda.compute (#3923)

* feat: add `index_rpad_and_clip` kernels implementation using cuda.compute

* style fix

* add a test for `index_rpad_and_clip_axis0` that would have target>length

* add keyword names

* style

* Apply suggestions from Ianna

Co-authored-by: Ianna Osborne <ianna.osborne@cern.ch>

* style: pre-commit fixes

---------

Co-authored-by: Ianna Osborne <ianna.osborne@cern.ch>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

* fix: array attrs not being validated at creation and being of inconsistent type (#3996)

* convert dict attrs to Attrs type

* add test

* fix: ensure jax backend uses arrays on cpu only (#3990)

* ensure jax uses cpu by default

* remove now redundant jax_platform_name setting

* change error messages

* better errors

* remove DeviceArray mentions as that does not exist while we're at it too

* almost there

* cleanup

* cleanup

* remove old data file

* update cupy kernels

* remove exlicit test for depricated kernel

* add complex kernels and port them to use offsets

* fix complex reducers

* add remaining kernels

* add kernels for complex and bool sum

* move to segmented_reduce

* fix typo

* promote type

* fix complex bool reducer

* try another algo

* missed one

* use type inference

* make numba happy

* remove reducer overloads

* use sum op func for complex types

* remove test of depricated code

* avoid using == to compare floating-point products

* fix boundary tests

* cleanup

* cleanup

* remove dead code

* revert lexsort to argmin/max

* handrolled lexsort

* remove dead code

* remove bincount

* import numba cuda for jit

* fix tests

---------

Co-authored-by: maxymnaumchyk <maxymnaumchyk@gmail.com>
Co-authored-by: maxymnaumchyk <70752300+maxymnaumchyk@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Iason Krommydas <iason.krom@gmail.com>
@ianna ianna requested a review from ikrommyd May 16, 2026 19:57

@ianna ianna left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ikrommyd - please take your time to review this. Thanks!

@codecov

codecov Bot commented May 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.20853% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.99%. Comparing base (4744cfc) to head (1d3875d).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/awkward/contents/listoffsetarray.py 92.10% 3 Missing ⚠️
src/awkward/contents/unionarray.py 60.00% 2 Missing ⚠️
src/awkward/_connect/jax/reducers.py 95.65% 1 Missing ⚠️
src/awkward/_kernels.py 75.00% 1 Missing ⚠️
src/awkward/_reducers.py 96.15% 1 Missing ⚠️
Additional details and impacted files
Files with missing lines Coverage Δ
src/awkward/_backends/cupy.py 100.00% <100.00%> (+4.05%) ⬆️
src/awkward/_connect/cuda/__init__.py 88.11% <100.00%> (-5.83%) ⬇️
src/awkward/_connect/cuda/_compute.py 44.27% <ø> (-16.94%) ⬇️
src/awkward/_connect/cuda/reducers.py 61.11% <100.00%> (-19.89%) ⬇️
src/awkward/_do.py 84.14% <100.00%> (-0.56%) ⬇️
src/awkward/contents/bitmaskedarray.py 76.58% <100.00%> (ø)
src/awkward/contents/bytemaskedarray.py 88.61% <100.00%> (-0.03%) ⬇️
src/awkward/contents/content.py 77.45% <100.00%> (ø)
src/awkward/contents/emptyarray.py 78.28% <100.00%> (ø)
src/awkward/contents/indexedarray.py 87.00% <100.00%> (+1.64%) ⬆️
... and 12 more

@ikrommyd

Copy link
Copy Markdown
Member

@ariostas should definitely take a look too (when he's back to wor). I do not fully trust myself with c++ yet but can definitely review the python part changes.

@ikrommyd

Copy link
Copy Markdown
Member

One question I have is how are the kernel test data generated? I'm not familiar with this part.
Are they generated from running the kernels? i.e if you have a bug in a kernel, will it propagate to the test data jsons? Or we can trust the test data for kernel correctness?

@ianna

ianna commented May 16, 2026

Copy link
Copy Markdown
Member Author

One question I have is how are the kernel test data generated? I'm not familiar with this part. Are they generated from running the kernels? i.e if you have a bug in a kernel, will it propagate to the test data jsons? Or we can trust the test data for kernel correctness?

yes, we can trust the data because these are the data from the tests.

@ikrommyd

Copy link
Copy Markdown
Member

I've ran this against HiggsDNA and getting the same results for my analysis. Only small floating point differences that np.isclose coverts (but array_equal errors). I'm pretty sure the kernel changes can cause such numerical differeneces.

@ianna

ianna commented May 17, 2026

Copy link
Copy Markdown
Member Author

I've ran this against HiggsDNA and getting the same results for my analysis. Only small floating point differences that np.isclose coverts (but array_equal errors). I'm pretty sure the kernel changes can cause such numerical differeneces.

Thanks @ikrommyd ! Floating point comparison is indeed a tricky one.

@ikrommyd

Copy link
Copy Markdown
Member

Yeah totally reasonable. Floating point ops are not commutative.

Anyways I'll test this out a bit more with analyses examples and I also wanna if there's any noticeable performance hit.
Then I'll actually read the code.

@ianna

ianna commented May 17, 2026

Copy link
Copy Markdown
Member Author

Yeah totally reasonable. Floating point ops are not commutative.

Anyways I'll test this out a bit more with analyses examples and I also wanna see if there's any noticeable performance hit. Then I'll actually read the code.

Thanks! Don’t worry about the performance yet — that’s the next on my list. First we must insure that the results are correct.👍

@github-actions

Copy link
Copy Markdown

The documentation preview is ready to be viewed at http://preview.awkward-array.org.s3-website.us-east-1.amazonaws.com/PR4056

ianna and others added 3 commits May 18, 2026 23:50
* feat: add `awkward_IndexedArray_overlay_mask` kernel using cuda.compute

* feat: add `awkward_IndexedArray_reduce_next_64` kernel using cuda.compute

* feat: add `IndexedArray_reduce_next_nonlocal_nextshifts_64` kernel using cuda.compute

* feat: add `ByteMaskedArray_getitem_nextcarry` kernel using cuda.compute

* feat: add `awkward_ByteMaskedArray_numnull` kernel using cuda.compute

* feat: add `awkward_RegularArray_getitem_jagged_expand` kernel using cuda.compute

* add an upper bound

* style: pre-commit fixes

* feat: add `awkward_RegularArray_getitem_jagged_expand` kernel using cuda.compute

* feat: add `awkward_UnionArray_simplify_one` kernel using cuda.compute

* feat: add `awkward_ListArray_broadcast_tooffsets` kernel using cuda.compute

* feat: add `awkward_ListArray_localindex` kernel using cuda.compute

* fix the impl

* feat: add `awkward_ListArray_compact_offsets` kernel using cuda.compute

* feat: add `awkward_ListArray_combinations_length` kernel using cuda.compute

* feat: add `awkward_ListArray_combinations` kernel using cuda.compute

* feat: add `awkward_UnionArray_nestedfill_tags_index` kernel using cuda.compute

* fix the tests for kernels that are deliberately raising errors

* compare `starts` and `stops` separately

* ignore `memptr` argument for pylint

* style: pre-commit fixes

* Add functions for indexing and repeating arrays

* style: pre-commit fixes

* return unary_transform call for segment_ids

* style: pre-commit fixes

* update the `awkward_IndexedArray_reduce_next_64` to work with offsets

* style: pre-commit fixes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ianna Osborne <ianna.osborne@cern.ch>
* feat: add reimplementation of the `awkward_RegularArray` kernels using cuda.compute

* reimplement more kernels using cuda.compute

* reimplement more kernels using cuda.compute

* style: pre-commit fixes

* style fix

* use the named functions instead of lambdas

* add new kernels

* add new kernels

* fix a type mismatch

* style: pre-commit fixes

* add new kernels

* fix the overflow error

* new batch of kernels (draft)

* add comments and cleanup

* fix the tests messages

* add new `BitMaskedArray` kernels

* modify the `test_1381_check_errors.py` test

* resolve the conflicts

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
@ikrommyd ikrommyd requested a review from ariostas May 28, 2026 03:12
@ariostas

Copy link
Copy Markdown
Member

@ianna what's the plan with this PR? Is this something that you would want to merge soon or is it to keep like a live comparison of the "Awkward3" changes?

Don’t worry about the performance yet

If this is going in main, we should worry about performance.

@ianna

ianna commented May 28, 2026

Copy link
Copy Markdown
Member Author

@ianna what's the plan with this PR? Is this something that you would want to merge soon or is it to keep like a live comparison of the "Awkward3" changes?

The plan is to discuss it at AS meeting.

Don’t worry about the performance yet

If this is going in main, we should worry about performance.

ASIS performance is good enough. And we already know the bottlenecks -- where we can optimize it -- I want to have confirmation from the users that validation passes.

@ikrommyd

ikrommyd commented Jun 4, 2026

Copy link
Copy Markdown
Member

What I'd like to see here is a benchmark showing that all or most of the high level ops (ak.sum, ak.max etc) are either the same speed or better with this change. The reason that we should benchmark high level ops is that I saw for example that the awkward reduction kernel itself is now slower because it has a double loop instead of a single one, but a high level reduction operation is faster now because it doesn't need to go from offsets to parents and then back to offsets.

It is important to know this before we can merge this change. The benchmarks can be done either with synthetic randomly generated data or a reasonably big root file (between 100K and 500K events which is the reasonable analysis chunk size atm).

@ianna ianna changed the title feat: complete CPU kernel migration from parents to offsets (#3988) feat: complete CPU kernel migration from parents to offsets Jun 8, 2026
@ianna

ianna commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

@TaiSakuma -- could you please check the test failure? It's an EmptyArray size related to the hypothesis tests I think.

@ikrommyd

Copy link
Copy Markdown
Member

@ianna @TaiSakuma this is a reproducer of the CI failure. Didn't have time to look if such an array is bad enough that should never be generated by hypothesis-awkward or whether we have a bug in awkward code.

import awkward as ak
import numpy as np

layout = ak.contents.UnionArray(
    ak.index.Index8(np.empty(0, dtype=np.int8)),
    ak.index.Index64(np.empty(0, dtype=np.int64)),
    [
        ak.contents.BitMaskedArray(
            ak.index.IndexU8(np.empty(0, dtype=np.uint8)),
            ak.contents.EmptyArray(),
            valid_when=True,
            length=0,
            lsb_order=False,
        ),
        ak.contents.BitMaskedArray(
            ak.index.IndexU8(np.empty(0, dtype=np.uint8)),
            ak.contents.EmptyArray(),
            valid_when=True,
            length=0,
            lsb_order=False,
        ),
    ],
)

ak.array_equal(layout, layout)

@henryiii

Copy link
Copy Markdown
Member

🤖 AI text below 🤖

All four audits are complete. Here's the full review.


Review: PR #4056 — feat: complete CPU kernel migration from parents to offsets

Overview

This PR rewrites the reducer/sort/unique machinery so that kernels receive contiguous offsets ranges instead of per-element parents labels: ~100 CPU kernels rewritten (with optional OpenMP parallelism over bins), the YAML spec and test data migrated, all 14 content classes re-threaded with the new _reduce_next/_sort_next/_unique signatures, EmptyIndex/ZeroIndex deleted, and the CUDA side moved almost entirely onto cuda.compute. The architecture is sound and the core work is high quality — the rewritten CPU reducer kernels match the spec exactly (verified side-by-side for every reducer, including identities, empty bins, NaN semantics, complex interleaving, and argmin/argmax absolute-index conventions; all changed files compile cleanly against the spec-generated kernels.h). But there is one real CUDA correctness bug, a missing required version bump, and a cluster of problems around the new awkward_Index_parents_to_offsets kernel.

Blocking issues

  1. Missing awkward-cpp version bump. The PR changes ~100 C++ files, but awkward-cpp/pyproject.toml and the root pyproject.toml pin are both still 53 — identical to main, where 53 is already released. Per the project's own convention this must be bumped to 54 in both files (and an awkward-cpp release made) before merge.

  2. CUDA bug in awkward_RegularArray_reduce_nonlocal_preparenext_64.cu:51. The device code still writes nextcarry in the old global column-major order (nextcarry[scan[j*length + i] - 1] = i*size + j), while the host-computed nextoffsets assumes the new per-(bin, j) grouping that the CPU kernel and spec use. The orderings coincide only for outlength == 1; for nested reductions (outlength > 1, e.g. depth-3 arrays reduced on the innermost axis) CUDA produces wrong results. Counterexample: offsets=[0,1,2], size=2, length=2 → CPU nextcarry=[0,1,2,3], CUDA [0,2,1,3] with identical nextoffsets. Compounding this, the same kernel is the only one whose kernel-test-data.json entry was deleted instead of migrated, so it now has zero generated test coverage on both backends. (The searchsorted-derived parents in its host block is also dead.)

  3. The new awkward_Index_parents_to_offsets kernel is broken and unused. The spec's reference zeroes only tooffsets[0..outlength-1] but increments tooffsets[p+1], leaving the last slot uninitialized (range(outlength) should be range(outlength + 1)); the C++ does no zeroing and silently requires a caller-zeroed buffer (Awkward allocates with empty); negative parents are unguarded (p < -1 writes out of bounds); the sorted/non-negative/in-range preconditions are undocumented; and it has no caller and no tests. Either fix, document, and test it — or drop it from this PR.

  4. OOB read in awkward_sort.cpp:54. offsets[offsetslength - 1] is read unconditionally; offsetslength == 0 reads offsets[-1]. The old kernel used the (now-ignored but still-present) parentslength parameter and was safe. Add a guard, and consider clamping offsets[i+1] against length in the sort loop while there.

Likely bugs / decisions needed

  • recordarray.py:994shifts silently ignored for positional record reducers. Dropping the double starts adjustment is justified and tested, but when an option layer above produced non-None shifts (Nones inside lists), the old code corrected positions and the new code does nothing. Needs a test: argmin via a record behavior on [[{...}, None, {...}]] with the None before the minimum.
  • listoffsetarray.py:957 — wrong invariant in _unique. Asserts rows == offsets.length - 1 (i.e. outlength), but the parents-era equivalent is the value offsets[-1] — the adjacent comment even says so. Currently unreachable via the public API, but it will fire spuriously when axis support widens.
  • All 21 rewritten awkward_reduce_*.cu kernels are dead code. They were carefully (and, as far as sampled, correctly) migrated, yet every one is commented out of cuda_kernels_impl in dev/generate-kernel-signatures.py, so dispatch always falls through to cuda.compute. That silently makes CCCL Python a hard requirement for every reducer and ~70 structural kernels on the CUDA backend (previously CuPy alone sufficed), and it isn't declared anywhere in pyproject.toml. Re-enable them or delete them; don't ship maintained-but-unreachable code that's still concatenated into the runtime RawModule.
  • _connect/jax/reducers.py — 16 annotations still reference ak.index.EmptyIndex, which this PR deletes. Harmless under from __future__ import annotations until something evaluates the hints (sphinx, get_type_hints); change to ak.index.Index.

Cleanup (should fix before merge)

  • ~300 lines of commented-out parents-era implementations left in listoffsetarray.py (1028–1138, 1829–1920), regulararray.py (946–1016), and numpyarray.py (753–776). Git history preserves these; delete them.
  • LazyIndex in index.py now has zero subclasses/users after EmptyIndex/ZeroIndex removal.
  • Six parents-era kernels (sorting_ranges, reduce_local_nextparents_64, etc.) remain in the spec with no live CPU caller — decide keep-as-deprecated vs. drop.
  • Misleading comments in awkward_sort.cpp/awkward_argsort.cpp claim NaNs go to the high end "matching NumPy"; the comparator puts them at the low end (behavior unchanged from main — only the comment is wrong).
  • _compute.py:1781 raises a host-side Python error whose text still says "in compiled CUDA code" (kept deliberately so generated assertions match — worth a comment at least).
  • regulararray.py:1233: assert offsets.length == outlength + 1 will raise TypeError (not skip) if exactly one side is unknown_length; guard like the rest of the codebase.
  • generate-tests.py: hoist the hardcoded CUDA_COMPUTE_KERNELS set out of the innermost loop, and note it must be updated whenever an error-raising kernel moves to cuda.compute.

Test coverage

For a +35.8k/−39.1k PR, the hand-written test delta is tiny: one CPU test updated, three CUDA tests adjusted, one deleted (justified — it asserted dispatch machinery that no longer exists), and no new test files despite the project convention. The regenerated tests-spec/tests-cpu-kernels suites carry most of the load and the migrated kernel-test-data.json entries were validated programmatically (offsets well-formed, empty bins → identity, argmin/argmax absolute indices) — that's genuinely good — but the gaps above (preparenext_64, Index_parents_to_offsets, the RecordArray shifts path) all sit exactly where generated coverage is absent. Before merge: full suite + tests-spec/tests-cpu-kernels, the CUDA suites on real hardware (the preparenext bug will only show there), and a dask-awkward smoke test, since _reduce_next recursion changed shape.

Other notes

  • OpenMP: new optional find_package(OpenMP) in awkward-cpp — race-free (disjoint bins, gated if(outlength > 1024)), serial fallback is clean. But manylinux CI will find libgomp, so published wheels will quietly become threaded (auditwheel-bundled libgomp) — make that a deliberate decision, and consider oversubscription under dask.
  • Public-API removal: ak.index.EmptyIndex/ZeroIndex were importable in released 2.x; removal risk is low but nonzero for third parties.
  • tests-cuda/test_3149 fixed a genuine pre-existing out-of-bounds read in test_block_boundary_prod_complex13 — nice catch.
  • IndexedArray._unique replacing ~120 lines of dubious parents-era logic with carry-and-delegate is a real improvement; worth a maintainer's explicit sign-off that no covered case regresses.
  • PR body is just "back port from awkward3 branch" — for a change this large, the description should explain the offsets contract (offsets[0] == 0, offsets[outlength] == len, monotone), the OpenMP addition, and the CUDA dependency shift. Also: AI-assisted contribution disclosure if applicable, per CONTRIBUTING.md.

Verdict: the core migration is well executed and verifiably correct on the CPU side, but this shouldn't merge until the version bump, the RegularArray_reduce_nonlocal_preparenext_64.cu ordering bug (plus restored test coverage for it), and the Index_parents_to_offsets kernel issues are resolved, and a decision is made about the dead CUDA reducer kernels / hard cuda.compute dependency.

@henryiii

Copy link
Copy Markdown
Member

Pref analysis with Opus 4.8:

🤖 AI text below 🤖

CPU performance: better — substantially, for reductions

Yes, it's better. I built awkward-cpp + awkward from both main (before) and awkward3 (after) into separate venvs, then benchmarked the reduction/sort kernels affected by this branch's core change (PR #3988: migrating the CPU reducer kernels from a parents array to an offsets array for segmented reductions). Results verified correct on both.

Overall geomean: ~4.1× faster (after vs. before), across 3 data shapes × the reduction ops.

The big wins — segmented reductions along axis=1

These are the kernels the PR rewrote, and they're dramatically faster:

Op Typical speedup
sum / prod 2–19×
max / min 2–18×
count_nonzero 1.7–14×
argmax / argmin (int64) 1.8–3.7×
sum (complex128) 3–12×
any / all / sum (bool) 2–700×

The improvement is biggest for "few long lists" and "balanced" shapes (the old parents-based code scaled poorly there); even the hardest case, "2M short lists," is still 1.7–4× faster. The extreme bool any/all numbers (e.g. 730×) are a genuine but somewhat special case — the new offsets-based path has an effectively free fast path for boolean reductions, so I'd treat those as outliers rather than the headline.

The one regression — sort / argsort

These are mixed-to-slightly-worse:

  • sort axis=1: 0.79–0.92× (up to ~20% slower) for balanced/long-list shapes, but 1.25–1.28× faster for many-short-lists.
  • argsort axis=1: roughly neutral (0.93–1.05×), faster only for many-short-lists.
  • A couple of float64 argmax/argmin cases in the long-list shape are ~8% slower.

So sort/argsort didn't benefit from the migration the way the reducers did, and lost a bit on long-list workloads.

Bottom line

For the operations this PR targets (segmented reductions), CPU performance is much better — typically several times faster, with no reduction regressions. The only thing to watch is sort/argsort, which is roughly neutral overall but ~10–20% slower on long-list inputs.

Caveats: single Apple-silicon machine, best-of-7 timings, Python 3.13. I couldn't test GPU (no CUDA here), and this measures only the kernels touched by the branch, not end-to-end workloads. The two built venvs are at /tmp/v_before and /tmp/v_after and the benchmark is /tmp/bench_cpu.py if you want to re-run or extend it.

@ianna

ianna commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

@henryiii — fantastic! Can you ask for the memory profile comparison as well?

@ikrommyd

Copy link
Copy Markdown
Member

@henryiii what did the agent benchmark? Cause the kernels themselves change but also the number of how many we need to run for each op might change too. We'd want to benchmark highlevel ops like ak.sum not only the kernels on their own.

@henryiii

Copy link
Copy Markdown
Member

Here's the benchmark script (/tmp/bench_cpu.py):

"""CPU reduction/sort benchmark targeting the parents->offsets kernel migration."""
import gc
import sys
import time

import numpy as np
import awkward as ak

print("awkward", ak.__version__, "| awkward_cpp", getattr(__import__("awkward_cpp"), "__version__", "?"))

rng = np.random.default_rng(12345)


def make_ragged(n_lists, avg_len, dtype):
    counts = rng.integers(0, 2 * avg_len + 1, size=n_lists)
    total = int(counts.sum())
    if dtype == bool:
        data = rng.integers(0, 2, size=total).astype(bool)
    elif dtype == np.complex128:
        data = (rng.standard_normal(total) + 1j * rng.standard_normal(total)).astype(np.complex128)
    elif np.issubdtype(dtype, np.integer):
        data = rng.integers(-1000, 1000, size=total).astype(dtype)
    else:
        data = rng.standard_normal(total).astype(dtype)
    offsets = np.zeros(n_lists + 1, dtype=np.int64)
    offsets[1:] = np.cumsum(counts)
    layout = ak.contents.ListOffsetArray(
        ak.index.Index64(offsets), ak.contents.NumpyArray(data)
    )
    return ak.Array(layout)


def bench(fn, arr, repeat=7, inner=3):
    # warmup
    fn(arr)
    best = float("inf")
    for _ in range(repeat):
        gc.disable()
        t0 = time.perf_counter()
        for _ in range(inner):
            fn(arr)
        t1 = time.perf_counter()
        gc.enable()
        best = min(best, (t1 - t0) / inner)
    return best


# scales: (n_lists, avg_len)
SCALES = {
    "few_long": (1_000, 10_000),     # few segments, long lists
    "many_short": (2_000_000, 5),    # many segments, short lists
    "balanced": (200_000, 50),       # balanced
}

OPS = []
def reg(name, fn, dtypes):
    OPS.append((name, fn, dtypes))

reg("sum_axis1", lambda a: ak.sum(a, axis=1), [np.int64, np.float64])
reg("sum_axisNone", lambda a: ak.sum(a, axis=None), [np.int64, np.float64])
reg("prod_axis1", lambda a: ak.prod(a, axis=1), [np.int64, np.float64])
reg("max_axis1", lambda a: ak.max(a, axis=1), [np.int64, np.float64])
reg("min_axis1", lambda a: ak.min(a, axis=1), [np.int64, np.float64])
reg("argmax_axis1", lambda a: ak.argmax(a, axis=1), [np.int64, np.float64])
reg("argmin_axis1", lambda a: ak.argmin(a, axis=1), [np.int64, np.float64])
reg("count_nonzero_axis1", lambda a: ak.count_nonzero(a, axis=1), [np.int64, np.float64])
reg("any_axis1", lambda a: ak.any(a, axis=1), [bool])
reg("all_axis1", lambda a: ak.all(a, axis=1), [bool])
reg("sum_bool_axis1", lambda a: ak.sum(a, axis=1), [bool])
reg("sum_complex_axis1", lambda a: ak.sum(a, axis=1), [np.complex128])
reg("sort_axis1", lambda a: ak.sort(a, axis=1), [np.int64, np.float64])
reg("argsort_axis1", lambda a: ak.argsort(a, axis=1), [np.int64, np.float64])

results = {}
for scale_name, (n_lists, avg_len) in SCALES.items():
    cache = {}
    for name, fn, dtypes in OPS:
        for dt in dtypes:
            key = id(dt) if not isinstance(dt, type) else dt
            if dt not in cache:
                cache[dt] = make_ragged(n_lists, avg_len, dt)
            arr = cache[dt]
            dtn = np.dtype(dt).name
            t = bench(fn, arr)
            results[(scale_name, name, dtn)] = t
            print(f"{scale_name:12s} {name:22s} {dtn:12s} {t*1e3:10.3f} ms", flush=True)
    del cache
    gc.collect()

# dump machine-readable
import json
with open(sys.argv[1] if len(sys.argv) > 1 else "/tmp/bench_out.json", "w") as f:
    json.dump({f"{k[0]}|{k[1]}|{k[2]}": v for k, v in results.items()}, f, indent=2)
print("WROTE", sys.argv[1] if len(sys.argv) > 1 else "/tmp/bench_out.json")

A few notes on the methodology choices:

  • Same script, two interpreters. I ran it under /tmp/v_before/bin/python (built from main) and /tmp/v_after/bin/python (built from awkward3), writing /tmp/before.json and /tmp/after.json. The comparison script that produced the table is separate (an inline python3 - <<EOF heredoc) and just divides the two JSONs.

  • Timing: best-of-7, each measurement averaging 3 inner calls, with one warmup and GC disabled during timing — so it reports the fastest stable time, minimizing OS/GC jitter.

  • Fixed seed (default_rng(12345)), but note the rng is shared and consumed in order, so the exact arrays depend on op/dtype registration order. It's deterministic across runs (so before vs. after see identical data), but the three dtypes within a scale aren't drawn from the same stream position. Fine for a relative comparison; worth knowing if you re-use it.

  • As discussed, these are high-level ak.* calls — Python dispatch + kernel, not isolated kernels.

The comparison/analysis snippet wasn't saved to a file — it was run inline. Want me to save a cleaned-up version of both the benchmark and the comparison logic into the repo (e.g. under studies/) so this is reproducible?

@TaiSakuma

Copy link
Copy Markdown
Member

🤖 This comment was written by Claude Code.

Changes to existing tests

These are findings on the changes this PR makes to existing test files (modified files only; the newly added test_4056_* files are out of scope here). For each modified file the changes to expected values and assertions are listed, the test is classified as exercising public API or internal behavior, and the justification status of each public-API change is recorded. Verified against head 889dcd45.

Inventory

Four existing test files are modified, plus two internal test-infrastructure files:

File Δ Class
tests/test_2512_record_array_carry.py +17 −6 public op output, internal-representation assertion
tests-cuda/test_1381_check_errors.py +1 −14 internal (CUDA-specific error diagnostics)
tests-cuda/test_3149_complex_reducers.py +59 −28 public API (CUDA reducers)
tests-cuda/test_3162_block_boundary_reducers.py +58 −24 public API (CUDA reducers)
kernel-test-data.json +27597 −28811 internal (kernel snapshot data)
dev/generate-tests.py +40 −12 internal (test generator)

Changes to expected values and assertions

tests/test_2512_record_array_carry.py — Two hardcoded expected_result layouts (test_axis_0, test_axis_1) are rewritten: the IndexedOptionArray.index changes from [1, 5, 9, 3, 7, 10] to [1, 3, 5, 7, 9, 10], and the two RecordArray NumpyArray contents are reordered. The assertion form is unchanged (result.is_equal_to(expected_result), a strict structural layout comparison). Dereferencing the old and new layouts through the index yields identical user-visible values in both cases — axis_0: [(6, 1.0), (7, 14.0), (8, 16.0), (9, 18.0), (10, 20.0), (5, 10.0)]; axis_1: [(3, 6.0), (4, 8.0), (5, 10.0), (9, 18.0), (10, 20.0), (8, 16.0)] — while is_equal_to returns False because the physical buffers differ. The change is a representation-only reshuffle of the reduce-nonlocal carry (the new index is monotonic); to_list, positional indexing, ==, and ak.array_equal are unaffected.

tests-cuda/test_1381_check_errors.py — The cp.cuda.Stream() block that queued three out-of-range slices and the deferred synchronize_cuda(stream) call are removed; the error is now raised eagerly inside the indexing call (v2_array_cuda[10,]). The asserted error message is trimmed to the core text index out of range in compiled CUDA code (awkward_RegularArray_getitem_next_at); the trailing slice-context lines are dropped. The core error (ValueError, out-of-range, kernel name) is still asserted, but the assertion is otherwise weaker and the raise is now eager rather than deferred. This matches the generator change below, which routes cuda.compute kernels to eager error raising.

tests-cuda/test_3149_complex_reducers.pytest_block_boundary_prod_complex13 changes its input from size=1000 to size=2000; the prior size produced only 500 complex values while the offsets indexed to 1000, so both backends read uninitialized memory and compared garbage. This is a fix to a broken test input, not an expected-value change. The argmax/argmin tests change assertion form only — == becomes int(...) == int(...) and to_list(...) == to_list(...) becomes np.testing.assert_array_equal(...); the asserted relationship (CUDA result equals CPU result) and the values are unchanged (integer-index results, exact). cp.get_default_memory_pool().free_all_blocks() cleanup is added.

tests-cuda/test_3162_block_boundary_reducers.pytest_block_boundary_prod relaxes its assertion from exact equality (ak.prod(cuda) == ak.prod(cpu)) to np.testing.assert_allclose(..., rtol=1e-14), and in the same edit changes the segmented bins from [0, 1, 2998, 3000] to [0, 1, 2261, 2262]. The any/all/prod_bool tests change assertion form only — == becomes bool(...) == bool(...) or assert_array_equal, and a np.bool_ dtype becomes bool; the asserted relationship and values are unchanged (bool/int results, exact). free_all_blocks() cleanup is added throughout.

kernel-test-data.json (internal) — Recorded kernel I/O snapshot data. Comparing head against main, in the explicit (unit-tests) section: 34 surviving kernels have changed recorded I/O (the migrated reducers/sort, e.g. awkward_reduce_sum drops parents/lenparents and keeps offsets; awkward_ListOffsetArray_reduce_nonlocal_preparenext_64 replaces parents with outer_offsets/outlength and output nextparents with nextoffsets), and 1 kernel is removed (awkward_RecordArray_reduce_nonlocal_outoffsets_64). The changes track the new parents→offsets signatures. The recorded outputs are stored test data, not an independent oracle; the spec suites check that the Python reference and the compiled C kernel both reproduce them (cross-implementation agreement), not independently-derived correctness.

dev/generate-tests.py (internal) — The test generator. Adds a module-level CUDA_COMPUTE_KERNELS set and a new eager-error branch in CUDA error-test generation, so kernels dispatched through cuda.compute wrap pytest.raises around the kernel call (eager) instead of around synchronize_cuda() (deferred). Removes the deleted kernel from one list.

Public API vs internal

Test change What is asserted Class
test_2512 expected layouts internal physical layout of a public op's output (is_equal_to); observable values invariant public API; internal-representation change
test_1381 error path CUDA-specific error diagnostics (message text, raise timing); public invariant (out-of-range → ValueError) unchanged internal (CUDA-specific)
test_3149 argmax/argmin, prod input public op results (CUDA vs CPU) public API
test_3162 prod/any/all public op results (CUDA vs CPU) public API
kernel-test-data.json, generate-tests.py kernel I/O snapshot, test tooling internal

Justification status of the public-API test changes

# Change Status Finding
A test_2512 layout rewrite ✅ justified Dereferenced values are byte-identical on both axes (verified); representation-only change, new index monotonic. No user-visible value or order change.
B test_3149 prod_complex13 input ✅ justified Genuine, documented bug fix — the prior input compared uninitialized memory.
C test_3149 / test_3162 argmax/argmin/any/all/bool refactors ✅ justified Equivalent assertion forms; no expected value changed; exact backend equality preserved.
D test_3162 prod ==assert_allclose(rtol=1e-14) ⚠️ not established by the diff Integer prod (int64, modular, order-independent on CPU), so concern 8's FP caveat (scoped by the PR body to float/complex, not integers) does not apply and exact == would still pass on CPU. Comparing CUDA vs CPU, the relaxation can only cover a cuda.compute integer-prod divergence from CPU that the diff does not document (untestable here without a GPU).
E test_3162 prod bins [0,1,2998,3000][0,1,2261,2262] ⚠️ not established by the diff The same edit relaxes the assertion and shrinks the inputs with no stated reason. The shrink is not a tolerance fix: both bins overflow int64, and the wrapped result actually grows (8.9e17 → 4.1e18), loosening the absolute rtol rather than tightening it. So the reason for the bin change cannot be determined from the diff.

Summary

Apart from the regenerated kernel-snapshot data, the only hand-written test expected value rewritten to match modified-kernel output is test_2512 (A), and it is a representation-only change with identical observable values. The value-preserving refactors (C) and the test_3149 bug fix (B) are justified. Two changes are not fully closed by the diff: the test_3162 prod assertion relaxation (D) and bin-size change (E), which together point to an undocumented cuda.compute prod divergence (the integer product is order-independent on CPU, so neither is explained there).

@TaiSakuma

Copy link
Copy Markdown
Member

I'm a bit confused. I thought at first D is where 8 is sufracing to the public API. But D seems to be for integers and has nothing to do with 8. There is no test that fails for 8. Does that mean all tests for the public API that 8 surfaces already use ak.almost_equal() and the impact of 8 is within the tolerance? If so, 8 is resolved. D is outstanding.

@TaiSakuma

Copy link
Copy Markdown
Member

🤖 This comment was written by Claude Code.

On concern 8 — floating-point reassociation

The offsets-pipeline reducer changes the order of summation, so float sum / prod (and ops derived from them) can return values that differ from released awkward at the floating-point rounding level. Integer reductions are unaffected (integer arithmetic is order-independent), as are min / max / arg* / count* (they do not depend on summation order). Verified against head 889dcd45.

How it surfaces. Comparing released awkward against this PR, exact equality (== / ak.array_equal) on a float sum / prod can differ while np.isclose passes — which is how it was reported.

Why CI stays green. The reduction tests compare float sum / prod against independent (hardcoded / NumPy) references using pytest.approx or assert_allclose — for example, tests/test_2020_reduce_axis_none.py asserts ak.sum(array, axis=None) == pytest.approx(63.0). The suite uses pytest.approx 45 times and assert_allclose 10 times in tests/, and all test jobs pass (the only red checks are the Codecov coverage thresholds, not test failures). So the existing tests already bound the change to within those tolerances; it appears only under exact comparison.

Magnitude. The difference stays within tolerance: np.isclose passes (exact == fails), and the existing reduction tests bound it to within their pytest.approx / assert_allclose tolerances for the inputs they exercise.

Disposition. This is an inherent consequence of the reassociation, accepted in review as not a bug. Because the shift stays within tolerance, nothing in the suite detects it — so a code change of this kind in an earlier release would have gone unnoticed as well, and exact float-reduction values have not been guaranteed stable across versions.

@TaiSakuma

Copy link
Copy Markdown
Member

🤖 This comment was written by Claude Code.

Status of the eight merge-condition items

Status of the eight items listed as conditions for merging — 1, 3, 6, 8, 19, 20, 23, 25 — as of head 889dcd45. Numbering follows the earlier concerns table. Code-checkable items were verified against the head; the rest reflect the 06-27 maintainer review.

# Concern Status
1 CUDA nonlocal-reduce carry ordering Closed — reported fixed and GPU-verified in the 06-27 review; the new .cu write matches the CPU reference. Residual: the CUDA kernel signature diverges from the CPU one (row_parents+length vs outlength), a cross-backend-consistency item per AGENTS.md, not a correctness bug.
3 OOB read in awkward_sort.cpp Closed — verified at head: awkward_sort.cpp:111 guards the empty-offsets read with (offsetslength > 0) ? offsets[offsetslength-1] : 0. Residual: the offsets[i+1] clamp is left to the offsets contract, by design.
6 regulararray TypeError on unknown_length Closed — verified at head: the reduce assert short-circuits when offsets.length or outlength is unknown_length, so the typetracer/dask path no longer raises.
8 Floating-point reassociation Closed — the change stays within tolerance and all tests pass; reductions are compared within tolerance, so a within-tolerance shift is not a regression. A release note is impractical, since no test detects a within-tolerance change. Detailed in a separate comment.
19 CCCL (cuda.compute) undeclared ⚠️ Open — decision. cccl appears in the GPU reducer path (src/awkward/_connect/cuda/reducers.py) but is absent from both pyproject.toml files; only CI installs it (setup-gpu-env, requirements-test-gpu.txt). Declaring it (e.g. a cuda extra) versus documenting it is a core-dependency policy call.
20 awkward-cpp version bump (53 → 54) Closed — the bump rides the next release (still version = "53" and awkward_cpp==53 at head); not a pre-merge blocker.
23 Removal of EmptyIndex / ZeroIndex / LazyIndex ⚠️ Open — decision. Verified at head: still removed, no deprecation shim. These classes shipped in only one release (v2.9.1, released 2026-06-08; absent from v2.9.0 and earlier). Within awkward they are used only in the private reduce/sort/unique recursion, and this PR also removes the parents parameter from those private methods (_reduce_next / _sort_next / _unique).
25 IndexedArray._unique rewrite sign-off Closed. tests/test_4056_indexedarray_unique.py (added in 889dcd45) covers the IndexedArray._unique / _is_unique rewrite, checking results against the projected (materialized) array plus known answers across reordering, dtypes, both axes, the empty fast path, and nesting under a list.

Where this leaves the eight

  • Closed: 1, 3, 6 (code fixes; 3 leaves the contract-reliant clamp by design), 8 (within tolerance; all tests pass), 20 (bumped at release), and 25 (regression test added).
  • Still open before merge: 19 (declare-or-document cccl), 23 (shim versus changelog).

@TaiSakuma

Copy link
Copy Markdown
Member

Thanks @ianna for the comment and commits.

So, 1, 3, 6, 8, 20, and 25 are closed. Two are open:

  • 19 - Decision needed. I'm not sure what to do with this.
  • 23 - I think this can be considered closed. They became public only in the previous release and have no practical usage for third-party code. In this PR, they are characterized as low-but-nonzero third-party risk. But in the future, we should introduce a proper deprecation and removal procedure and aim for zero risk.

@TaiSakuma

Copy link
Copy Markdown
Member

From my point of view,

  • 19 is outdtanding,
  • D and E need to be justified.

@ianna

ianna commented Jun 29, 2026

Copy link
Copy Markdown
Member Author

Thanks @ianna for the comment and commits.

So, 1, 3, 6, 8, 20, and 25 are closed. Two are open:

* **19** - Decision needed. I'm not sure what to do with this.

* **23** - I think this can be considered closed. They became public only in the previous release and have no practical usage for third-party code. In this PR, they are characterized as low-but-nonzero third-party risk. But in the future, we should introduce a proper deprecation and removal procedure and aim for zero risk.

@TaiSakuma - thank you so much for detailed review! IMHO about 19: for now we handle cccl the same way as we do cupy and the hand-written kernels will be removed in the next iteration.

For D and E: I reverted the flat case to == (exact for modular int prod), and replaced the prime-product inputs with values that don't overflow int64 so the segmented comparison actually exercises cross-block prod correctness instead of wraparound.

@ianna

ianna commented Jun 29, 2026

Copy link
Copy Markdown
Member Author

@TaiSakuma -- macos-latest, 3.10, full fails with: ERROR: awkward_cpp-53-cp310-cp310-macosx_26_0_arm64.whl is not a supported wheel on this platform. This is a new error it seems.

@TaiSakuma

Copy link
Copy Markdown
Member

I re-ran macos-latest, 3.10, full. It passed this time.

@TaiSakuma

Copy link
Copy Markdown
Member

🤖 This comment was written by Claude Code.

Status: 19, D, E

Status of concern 19 and of rows D and E from the test-changes comment, as of head 1d3875df5.

19 — CCCL (cuda.compute) dependency. Resolved by decision: cccl is handled the same way as cupy — an undeclared, user-installed GPU dependency — with the hand-written CUDA kernels to be removed in a later iteration.

D — test_3162 prod, exact-equality relaxation. Resolved in commit 1d3875df5: the flat prod assertion is back to exact ==. The prime-product input (which overflowed int64) is replaced by 2 * 3 * 5 * 7 * 11 * 13 = 30030, so the result is exact and checked against a known value.

E — test_3162 prod, bin/input change. Resolved in the same commit: the segmented offsets are restored to [0, 1, 2998, 3000] with non-overflowing content, compared exactly against a known result ([2, 1155, 13]), so the test exercises cross-block prod correctness rather than int64 wraparound. The GPU jobs pass at head 1d3875df5, confirming cuda.compute integer prod matches CPU.

@TaiSakuma TaiSakuma left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All concerns raised in this PR that risk breaking user code have been addressed.

I'll merge this tomorrow if it is still open and nobody objects.

Optionally, we can open an issue to track the remaining open concerns that have no possibility of breaking user code.

@ikrommyd ikrommyd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not actually requesting any changes here but I think @ariostas and me should do another pass here after all the recent commits and additionally I think this needs some physics validation too before it's merged. I can definitely run HiggsDNA and we can ask other framework authors to run their CIs against this branch.

Thank you Ianna and Tai for the recent progress and active development and feedback here.

@ianna ianna merged commit 64b8712 into main Jun 30, 2026
77 of 81 checks passed
@ianna ianna deleted the awkward3 branch June 30, 2026 05:25
@ianna

ianna commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

Not actually requesting any changes here but I think @ariostas and me should do another pass here after all the recent commits and additionally I think this needs some physics validation too before it's merged. I can definitely run HiggsDNA and we can ask other framework authors to run their CIs against this branch.

Thank you Ianna and Tai for the recent progress and active development and feedback here.

I had time and have merged the PR.

You had the opportunity to review the substantive changes prior to Friday, and the subsequent commits affected only the test suite. As this PR was blocking further merges in the workflow, integration at this stage was required. Please proceed with any additional validation on your side.

@ianna ianna restored the awkward3 branch June 30, 2026 09:22
@ikrommyd

ikrommyd commented Jun 30, 2026

Copy link
Copy Markdown
Member

What you actually said earlier above was to "Please post your thoughts here by the end of Friday" regarding whether the migration from parents to offsets should go into the main branch to which I voted yes. You did not say "you have until Friday to review and then I'll merge".

In any case, physics validation should definitely have taken place here. It's definitely not nice to potentially silently break physics results if there was a bug here. Luckily, coffea, dask-awkward and HiggsDNA are fine so far. We'll see what the integration tests say soon. It's not the responsibility of analysis framework maintainers to deal with the fallout if there are any silent bugs here. It's awkward's responsibility to ensure that this doesn't happen as much as we can with testing/reviewing.

The good part is that so far from testing for the past hour, I haven't seen anything yet.

@ianna

ianna commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

What you actually said earlier above was to "Please post your thoughts here by the end of Friday" regarding whether the migration from parents to offsets should go into the main branch to which I voted yes. You did not say "you have until Friday to review and then I'll merge".

In any case, physics validation should definitely have taken place here. It's definitely not nice to potentially silently break physics results if there was a bug here. Luckily, coffea, dask-awkward and HiggsDNA are fine so far. We'll see what the integration tests say soon. It's not the responsibility of analysis framework maintainers to deal with the fallout if there are any silent bugs here. It's awkward's responsibility to ensure that this doesn't happen as much as we can with testing/reviewing.

The good part is that so far from testing for the past hour, I haven't seen anything yet.

I think my earlier message could have been clearer. My intention was to ask for feedback on whether the migration should go into the main branch, not to promise that I would wait until Friday before merging. I can see how the wording could reasonably have been interpreted that way.

That said, the migration itself had been open for review for several weeks, followed our normal review process, and required a review before it could be merged. It wasn't a last-minute or silent change.

If your concern is about the more recent changes, those were confined to the test suite and were made in response to review and validation work. They didn't change the implementation itself.

I completely agree that avoiding regressions in downstream physics analyses is important. That's why the implementation had unit tests, underwent human review, received additional scrutiny from AI-assisted code reviews, and is now being validated against downstream projects. Integration testing is an important part of building confidence, and I'm glad coffea, dask-awkward, and HiggsDNA appear to be unaffected so far.

@ianna

ianna commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

What you actually said earlier above was to "Please post your thoughts here by the end of Friday" regarding whether the migration from parents to offsets should go into the main branch to which I voted yes. You did not say "you have until Friday to review and then I'll merge".
In any case, physics validation should definitely have taken place here. It's definitely not nice to potentially silently break physics results if there was a bug here. Luckily, coffea, dask-awkward and HiggsDNA are fine so far. We'll see what the integration tests say soon. It's not the responsibility of analysis framework maintainers to deal with the fallout if there are any silent bugs here. It's awkward's responsibility to ensure that this doesn't happen as much as we can with testing/reviewing.
The good part is that so far from testing for the past hour, I haven't seen anything yet.

I think my earlier message could have been clearer. My intention was to ask for feedback on whether the migration should go into the main branch, not to promise that I would wait until Friday before merging. I can see how the wording could reasonably have been interpreted that way.

That said, the migration itself had been open for review for several weeks, followed our normal review process, and required a review before it could be merged. It wasn't a last-minute or silent change.

If your concern is about the more recent changes, those were confined to the test suite and were made in response to review and validation work. They didn't change the implementation itself.

I completely agree that avoiding regressions in downstream physics analyses is important. That's why the implementation had unit tests, underwent human review, received additional scrutiny from AI-assisted code reviews, and is now being validated against downstream projects. Integration testing is an important part of building confidence, and I'm glad coffea, dask-awkward, and HiggsDNA appear to be unaffected so far.

Just to add one point on wording and interpretation: phrases like “silent bugs” and suggestions that physics validation “should definitely have taken place” can imply that no validation was done on our side. That is not accurate.

All changes went through the standard review process, including required approval before merge, and were supported by unit tests and additional validation work. Downstream integration testing is of course essential, but it is not correct to frame this as if there was no prior physics validation effort.

Going forward, I think it would help to be more precise in distinguishing between (1) potential gaps in downstream coverage and (2) the existence of validation and review before merge. Conflating the two makes the discussion harder than it needs to be.

@TaiSakuma

Copy link
Copy Markdown
Member

@ikrommyd - I'm curious to know why "physics validation should definitely have taken place" on this PR. How does the validation work? How do you run it? Are the validation tools public that I can look at?

@ikrommyd

Copy link
Copy Markdown
Member

The reason I say "silent bugs" is that the only thing that can actually be wrong in this PR when all the CIs are green is something that is untested and the result could have just changed without error without us knowing. I did not say anywhere that this hasn't been tested within the awkward world. It had tests and benchmarks but my understanding is that we didn't do any testing here of the broader scikit-hep ecosystem or analysis frameworks.

@TaiSakuma because the only thing that could potentially be wrong here is just silently wrong numbers. I don't think that it happened but if it did, it would be pretty bad for physicists. And that's why the broader scikit-hep ecosystem and analysis frameworks should have been validated. As to how to do this, for packages you just run their test suite from awkward installed from this branch. For more complicated physics frameworks you cannot so easily so you need to ask the maintainers to try out awkward from this branch and report back if they get identical physics results. This is what I do for HiggsDNA for example, I validate that the numbers are identical down to floating point precision in its output.

@ianna

ianna commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

The reason I say "silent bugs" is that the only thing that can actually be wrong in this PR when all the CIs are green is something that is untested and the result could have just changed without error without us knowing. I did not say anywhere that this hasn't been tested within the awkward world. It had tests and benchmarks but my understanding is that we didn't do any testing here of the broader scikit-hep ecosystem or analysis frameworks.

@TaiSakuma because the only thing that could potentially be wrong here is just silently wrong numbers. I don't think that it happened but if it did, it would be pretty bad for physicists. And that's why the broader scikit-hep ecosystem and analysis frameworks should have been validated. As to how to do this, for packages you just run their test suite from awkward installed from this branch. For more complicated physics frameworks you cannot so easily so you need to ask the maintainers to try out awkward from this branch and report back if they get identical physics results. This is what I do for HiggsDNA for example, I validate that the numbers are identical down to floating point precision in its output.

@ikrommyd — your comments came across as a personal attack. The claims about carelessness and “silent wrong numbers” are not supported by the facts. I need you to apologise for that framing.

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

Labels

type/feat PR title type: feat (set automatically)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants