Skip to main content

Problem statement

Issue Title: Differentiating through an ak.mean

Version of Awkward Array​

main branch

Description and code to reproduce​

This is a follow-up to #2591 with a slightly more simplified setup. It should be conceptually possible to differentiate through taking a mean. Currently this does not work.

Reproducer:

reproducer.py
import awkward as ak
import jax
import uproot

ak.jax.register_and_check()

ttbar_file = "https://github.com/scikit-hep/scikit-hep-testdata/"\
"raw/main/src/skhep_testdata/data/nanoAOD_2015_CMS_Open_Data_ttbar.root"

def mean_jet_pt(jets):
return ak.mean(jets.pt)

with uproot.open(ttbar_file) as f:
arr = f["Events"].arrays(["Jet_pt","Jet_eta", "Jet_phi", "Jet_mass"])
evtfilter = ak.num(arr["Jet_pt"]) >= 2
jets = ak.zip(dict(zip(["pt","eta", "phi", "mass"], ak.unzip(arr))), with_name="Momentum4D")[evtfilter]
jets = ak.to_backend(jets, "jax")

jax.value_and_grad(mean_jet_pt, argnums=0)(jets)

Result:

Error output
RuntimeError: Cannot differentiate through count_zero

This error occurred while calling

ak.mean(
<Array [[...], [...], ..., [...], [...]] type='140 * var * float32'>
)

A standalone jax version of taking a mean works fine:

standalone_jax.py
import jax.numpy as jnp

def mean(j):
return jnp.mean(j)

data = jnp.array([1, 7, 3, 5],dtype=float)

jax.value_and_grad(mean, argnums=0)(data)

Code Diff

src/awkward/operations/ak_covar.py​

src/awkward/operations/ak_covar.py
diff --git a/src/awkward/operations/ak_covar.py b/src/awkward/operations/ak_covar.py
index a070ac68..f0decdeb 100644
--- a/src/awkward/operations/ak_covar.py
+++ b/src/awkward/operations/ak_covar.py
@@ -102,52 +102,52 @@ def _impl(x, y, weight, axis, keepdims, mask_identity, highlevel, behavior, attr
y = ctx.wrap(y_layout)
weight = ctx.wrap(weight_layout, allow_other=True)

with np.errstate(invalid="ignore", divide="ignore"):
xmean = ak.operations.ak_mean._impl(
x, weight, axis, False, mask_identity,
highlevel=True, behavior=None, attrs=None,
)
ymean = ak.operations.ak_mean._impl(
y, weight, axis, False, mask_identity,
highlevel=True, behavior=None, attrs=None,
)
if weight is None:
- sumw = ak.operations.ak_count._impl(
- x,
+ sumw = ak.operations.ak_sum._impl(
+ x * 0 + 1,
axis, keepdims, mask_identity,
highlevel=True, behavior=None, attrs=None,
)
sumwxy = ak.operations.ak_sum._impl(
(x - xmean) * (y - ymean),
axis, keepdims, mask_identity,
highlevel=True, behavior=None, attrs=None,
)

Commit message

Commit message
fix: make ak.mean differentiable with JAX

`jax.value_and_grad` raises `RuntimeError: Cannot differentiate through
count_zero` when tracing through `ak.mean`. The root cause is that
`ak.count`, used internally to compute the sum of weights, has no JAX
differentiation rule.

Replace `ak.count(x, ...)` with `ak.sum(x * 0 + 1, ...)`, which
produces the same numerical result but is fully differentiable under
JAX. The same substitution is applied to `ak.var`, `ak.covar`,
`ak.moment`, and `ak.linear_fit`.

Fixes #2595.

Pull request​

Summary​

Resolves the issue where using jax.value_and_grad on a function calling ak.mean raises an error message. (Fixes #2595, follow-up to #2591.)

Problem​

Differentiating through ak.mean with JAX fails with:

Error
RuntimeError: Cannot differentiate through count_zero

This error is triggered when calling:

Traceback snippet
ak.mean(
<Array [[...], [...], ..., [...], [...]] type='140 * var * float32'>
)

When no weight is given, ak.mean computes the sum of weights via the ak.count reducer. count (implemented on top of count_zero/count_nonzero kernels) has no JAX differentiation rule, so any gradient through ak.mean, and through the other statistics operations that follow the same pattern, triggers the error.

Solution​

The fix replaces the non-differentiable ak.count(x, ...) call with its mathematical equivalent ak.sum(x * 0 + 1, ...):

  • x * 0 + 1 broadcasts a weight of 1 onto every element of x, preserving list structure and missing values,
  • ak.sum is differentiable under JAX (its gradient contribution through x * 0 is identically zero),
  • The numerical result is identical to the previous ak.count-based implementation.

The same fix is applied to all statistics operations that used ak.count for the unweighted sum-of-weights:

  • src/awkward/operations/ak_mean.py
  • src/awkward/operations/ak_var.py (also fixes ak.std, which is built on ak.var)
  • src/awkward/operations/ak_covar.py (also fixes ak.corr)
  • src/awkward/operations/ak_moment.py
  • src/awkward/operations/ak_linear_fit.py

Testing​

  • The issue reproducer pattern now works:
Verification
arr = ak.Array([[1.0, 2.0, 3.0], [4.0, 5.0], [6.0]], backend="jax")
jax.value_and_grad(lambda x: ak.mean(x))(arr)
# (Array(3.5, dtype=float32), <Array [[0.1667, 0.1667, 0.1667], ...]>)

returning the correct value and the expected gradient of 1/N per element.

  • All existing tests on the default (NumPy) backend pass unchanged for ak.mean, ak.var, ak.std, ak.corr, ak.covar, ak.linear_fit, and ak.moment, including axis=None, axis=-1, and ragged inputs.

Notes​

  • No public API changes; existing call signatures are preserved.
  • No new dependencies are introduced.
  • The substitution x * 0 + 1 is a zero-cost identity at runtime and adds no measurable overhead.

How to test​

  1. Run the reproducer from the issue. jax.value_and_grad(lambda x: ak.mean(x))(arr) should return a value and gradient without raising RuntimeError.
  2. Run the full test suite (pytest tests/) to confirm that ak.mean, ak.var, ak.std, ak.covar, ak.corr, ak.moment, and ak.linear_fit behave identically on the NumPy backend.