Skip to content

Commit 704298b

Browse files
thisiscamclaude
andcommitted
Bucket C: plumb effective_order through atan2, integer_pow, cumprod/max/min
These rules call jet() or _deriv_prop_propagate() internally and otherwise just forward to already-aware primitives. Pure plumbing: - _atan2_taylor: forward to two inner jet() calls and to _deriv_prop_propagate - _integer_pow_taylor: forward to inner jet() (both branches) - _cumulative_jet_rule: forward to inner jet() over associative_scan (registered for cumprod, cummax, cummin) Each gets _register_eff_order_rule(prim). Also classify the remaining rules. The fan-out rules (abs, max, min, gather, select_n, scatter-add, reduce_max, reduce_min) all use jax.vmap over the order axis — structurally linear, like add/sub. They don't need the effective_order plumbing because per-entry cost is O(1) and the contract permits garbage above the threshold. README updated to reflect both the expanded aware list (now 22 primitives) and this third bucket. Tests: extend tests/test_effective_order.py to cover all 5 new primitives (atan2, integer_pow, cumprod, cummax, cummin). 30 new parametric cases, all passing. Total suite: 638 passed, 5 skipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 6126dff commit 704298b

3 files changed

Lines changed: 46 additions & 18 deletions

File tree

‎README.md‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -242,19 +242,21 @@ combine with `jax.vmap`.
242242
correct values for these primitives, because the linear op computes
243243
every entry uniformly.
244244
- **Nonlinear with an `effective_order`-aware rule.** Currently
245-
`exp`, `expm1`, `log`, `log1p`, `pow`, `logistic`, `tanh`, `erf_inv`,
246-
`div`, `sin`, `cos`, `sinh`, `cosh`, `mul`, `dot_general`,
247-
`conv_general_dilated`, `erf`, and any future `def_deriv`-registered
248-
primitive. The convolution scan short-circuits iterations beyond
245+
`exp`, `expm1`, `log`, `log1p`, `pow`, `integer_pow`, `logistic`,
246+
`tanh`, `erf_inv`, `div`, `sin`, `cos`, `sinh`, `cosh`, `mul`,
247+
`dot_general`, `conv_general_dilated`, `erf`, `atan2`, `cumprod`,
248+
`cummax`, `cummin`, and any future `def_deriv`-registered primitive.
249+
The convolution scan short-circuits iterations beyond
249250
`effective_order`. Output entries above `effective_order` are
250251
unspecified — slice to `series_out[:effective_order]` before using
251252
them.
252-
- **Nonlinear without an `effective_order`-aware rule.** A few rules
253-
(`abs`, `max`, `min`, `atan2`, `integer_pow`, `cumprod`, `cummax`,
254-
`cummin`, the `reduce_*` family, `scatter-add`) currently compute the
255-
full `O(K²)` convolution regardless. `effective_order` is silently
256-
ignored on this path. These can be made aware with the same
257-
`_jet_scan` plumbing as the others — see the source for the pattern.
253+
- **Linear-style fan-out rules** (`abs`, `max`, `min`, `gather`,
254+
`select_n`, `scatter-add`, `reduce_max`, `reduce_min`): each rule
255+
applies an op independently to every series index via `vmap`, the
256+
same shape as `add`/`sub` in the linear bucket. They don't
257+
short-circuit, but they don't need to — the per-entry cost is O(1)
258+
and the contract is satisfied because garbage-in-garbage-out for
259+
indices above `effective_order` is fine.
258260
- Because output entries above `effective_order` are unspecified for the
259261
second category, treat the whole `series_out[effective_order:]` slice as
260262
garbage even if upstream linear ops would otherwise leave it intact.

‎jet_array/__init__.py‎

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,8 @@ def _dynamic_update_slice_jet_rule(primals_in, series_in, **params):
479479

480480

481481
def _cumulative_jet_rule(
482-
primals_in, series_in, *, axis: int, reverse: bool, combine_fn: Callable
482+
primals_in, series_in, *, axis: int, reverse: bool, combine_fn: Callable,
483+
_jet_effective_order=None,
483484
):
484485
# Irrespective of backend, we always use the parallel prefix scan
485486
# implementation when differentiating because reduce_window is not
@@ -488,13 +489,16 @@ def _cumulative_jet_rule(
488489
partial(lax.associative_scan, combine_fn, axis=axis, reverse=reverse),
489490
primals_in,
490491
series_in,
492+
effective_order=_jet_effective_order,
491493
)
492494

493495

494496
deflinear(lax.cumsum_p)
495497
jet_rules[lax.cumprod_p] = partial(_cumulative_jet_rule, combine_fn=lax.mul)
496498
jet_rules[lax.cummax_p] = partial(_cumulative_jet_rule, combine_fn=lax.max)
497499
jet_rules[lax.cummin_p] = partial(_cumulative_jet_rule, combine_fn=lax.min)
500+
for _p in (lax.cumprod_p, lax.cummax_p, lax.cummin_p):
501+
_register_eff_order_rule(_p)
498502

499503

500504
def def_deriv(prim, deriv):
@@ -799,14 +803,17 @@ def _pow_by_squaring(x, n):
799803
return x * _pow_by_squaring(x * x, (n - 1) / 2)
800804

801805

802-
def _integer_pow_taylor(primals_in, series_in, *, y):
806+
def _integer_pow_taylor(primals_in, series_in, *, y, _jet_effective_order=None):
803807
if y == 0:
804-
return jet(jnp.ones_like, primals_in, series_in)
808+
return jet(jnp.ones_like, primals_in, series_in,
809+
effective_order=_jet_effective_order)
805810
else:
806-
return jet(lambda x: _pow_by_squaring(x, y), primals_in, series_in)
811+
return jet(lambda x: _pow_by_squaring(x, y), primals_in, series_in,
812+
effective_order=_jet_effective_order)
807813

808814

809815
jet_rules[lax.integer_pow_p] = _integer_pow_taylor
816+
_register_eff_order_rule(lax.integer_pow_p)
810817

811818

812819
def _logistic_taylor(primals_in, series_in, _jet_effective_order=None, **_):
@@ -924,18 +931,22 @@ def body_fun(v_acc, k):
924931
_register_eff_order_rule(lax.log_p)
925932

926933

927-
def _atan2_taylor(primals_in, series_in):
934+
def _atan2_taylor(primals_in, series_in, _jet_effective_order=None):
928935
x, y = primals_in
929936
primal_out = lax.atan2(x, y)
930937

931-
x, series = jet(lax.div, primals_in, series_in)
938+
x, series = jet(lax.div, primals_in, series_in,
939+
effective_order=_jet_effective_order)
932940
one = lax_internal._const(x, 1)
933-
c0, cs = jet(lambda x: lax.div(one, 1 + lax.square(x)), (x,), (series,))
934-
tail = _deriv_prop_propagate(c0, cs, x, series)
941+
c0, cs = jet(lambda x: lax.div(one, 1 + lax.square(x)), (x,), (series,),
942+
effective_order=_jet_effective_order)
943+
tail = _deriv_prop_propagate(c0, cs, x, series,
944+
effective_order=_jet_effective_order)
935945
return primal_out, tail
936946

937947

938948
jet_rules[lax.atan2_p] = _atan2_taylor
949+
_register_eff_order_rule(lax.atan2_p)
939950

940951

941952
def _div_taylor_rule(primals_in, series_in, _jet_effective_order=None, **_):

‎tests/test_effective_order.py‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ def _safe_x0(name):
4343
"cos": 0.5,
4444
"sinh": 0.4,
4545
"cosh": 0.4,
46+
"atan2": 0.5,
47+
"integer_pow": 0.7,
48+
"cumprod": 0.6,
49+
"cummax": 0.4,
50+
"cummin": 0.4,
4651
}.get(name, 0.5)
4752

4853

@@ -80,6 +85,16 @@ def _univariate_fn(name):
8085
return lambda x: lax.pow(x, jnp.float64(1.7))
8186
if name == "div":
8287
return lambda x: 2.0 / (1.0 + x)
88+
if name == "atan2":
89+
return lambda x: lax.atan2(x, jnp.float64(2.0))
90+
if name == "integer_pow":
91+
return lambda x: x ** 3
92+
if name == "cumprod":
93+
return lambda x: lax.cumprod(jnp.stack([x, x * 2.0, x * 3.0]))
94+
if name == "cummax":
95+
return lambda x: lax.cummax(jnp.stack([x * 1.5, x * 0.5, x * 2.0]))
96+
if name == "cummin":
97+
return lambda x: lax.cummin(jnp.stack([x * 1.5, x * 0.5, x * 2.0]))
8398
return None
8499

85100

0 commit comments

Comments
 (0)