Skip to content

Commit 4ccb054

Browse files
thisiscamclaude
andcommitted
def_comp + thread effective_order across jit boundaries
Two coupled fixes that together make effective_order honor the contract uniformly across user code, including jnp.* operators and explicit jit subroutines. (1) def_comp was discarding effective_order. The wrapper used partial(jet, comp), which passed effective_order=None to the inner jet call regardless of what the trace was told. comp_rule now accepts _jet_effective_order and forwards it; def_comp also calls _register_eff_order_rule(prim) so the dispatcher actually feeds the kwarg. (2) The deeper bug: _pjit_jet_rule discarded effective_order at every jit boundary. Any function using jnp.* primitives (which wrap each op in implicit jit) computed the full series despite the hint. Fix: when effective_order is non-None, append it as an extra runtime input to the inner pjit. _jet_jaxpr now has a propagate_effective_order flag that selects between the existing no-propagation path and a new path that uses jet_fun_with_eff + traceable_with_eff to pull eff out of the last input and feed it into the inner JetTrace. The jaxpr cache now distinguishes the two paths so a function jet'd with and without effective_order builds two cached jaxprs (instead of polluting one). Both jet_fun_with_eff and traceable_with_eff are linear-util transformations mirroring jet_fun and traceable respectively, with the only difference being that they thread the eff scalar through the trace. The inner JetTrace's effective_order then becomes a tracer on the inner pjit's eff input — lax.cond inside _jet_scan handles tracer-valued conditions natively. Tests: tests/test_effective_order.py adds 8 cross-jit propagation cases — 7 with implicit jit (jnp.exp, jnp.sin, x**0.5, x*x, x**3, … each composed with jnp.exp so the high-order Taylor coefficients are non-zero and truncation is observable) plus one with an explicit jax.jit subroutine. Each asserts both prefix correctness AND that the tail is genuinely truncated (zero), distinguishing actual short-circuiting from natural-zero coefficients. Total suite: 646 passed, 5 skipped. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 704298b commit 4ccb054

3 files changed

Lines changed: 152 additions & 11 deletions

File tree

‎README.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,13 @@ combine with `jax.vmap`.
232232

233233
### Caveats
234234

235+
- `effective_order` propagates across both explicit (`jax.jit`) and
236+
implicit (`jnp.*` operator) jit boundaries. The implementation binds
237+
`effective_order` as an extra runtime input to each inner pjit so the
238+
inner `JetTrace` reads it as a tracer and threads it into every
239+
primitive rule's `_jet_scan` call. No special action needed from the
240+
user — write your function in plain `jnp` style and the hint is
241+
honored throughout.
235242
- The primitives a JAX program uses fall into three categories:
236243
- **Linear / zero-derivative** (`add`, `sub`, multiplication by a
237244
constant, `neg`, broadcasting, slicing, comparisons, …): the operation
@@ -257,6 +264,10 @@ combine with `jax.vmap`.
257264
short-circuit, but they don't need to — the per-entry cost is O(1)
258265
and the contract is satisfied because garbage-in-garbage-out for
259266
indices above `effective_order` is fine.
267+
- One-time cost: when `effective_order` is set, each inner pjit re-traces
268+
with one extra input (the eff scalar). This adds a small compile-time
269+
cost the first time a function is `jet`'d with `effective_order` set vs
270+
without — both jaxprs are cached.
260271
- Because output entries above `effective_order` are unspecified for the
261272
second category, treat the whole `series_out[effective_order:]` slice as
262273
garbage even if upstream linear ops would otherwise leave it intact.

‎jet_array/__init__.py‎

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,34 @@ def traceable(f, store, in_tree_def, *primals_and_series):
261261
return out_flat
262262

263263

264+
@lu.transformation2
265+
def jet_fun_with_eff(f, order, primals, series, eff):
266+
"""Like jet_fun, but pulls effective_order from a runtime input."""
267+
tag = core.TraceTag()
268+
out_primals, out_terms = f(tag, order, eff, primals, series)
269+
out_terms = [
270+
jnp.zeros((order,) + p.shape) if s is zero_series else s
271+
for p, s in zip(out_primals, out_terms)
272+
]
273+
return out_primals, out_terms
274+
275+
276+
@lu.transformation_with_aux2
277+
def traceable_with_eff(f, store, in_tree_def, *all_inputs):
278+
"""Like traceable, but the LAST input is the effective_order scalar.
279+
280+
Used by _pjit_jet_rule when threading effective_order across a jit
281+
boundary: the inner jaxpr accepts one extra runtime input, and the
282+
inner JetTrace reads its effective_order from that input.
283+
"""
284+
*primals_and_series, eff = all_inputs
285+
primals_in, series_in = tree_unflatten(in_tree_def, primals_and_series)
286+
primals_out, series_out = f(primals_in, series_in, eff)
287+
out_flat, out_tree_def = tree_flatten((primals_out, series_out))
288+
store.store(out_tree_def)
289+
return out_flat
290+
291+
264292
class JetTracer(core.Tracer):
265293
__slots__ = ["primal", "terms"]
266294

@@ -580,7 +608,11 @@ def def_comp(prim, comp, **kwargs):
580608
"""
581609
Define the jet rule for a primitive in terms of a composition of simpler primitives.
582610
"""
583-
jet_rules[prim] = partial(jet, comp, **kwargs)
611+
def comp_rule(primals_in, series_in, _jet_effective_order=None, **_):
612+
return jet(comp, primals_in, series_in,
613+
effective_order=_jet_effective_order, **kwargs)
614+
jet_rules[prim] = comp_rule
615+
_register_eff_order_rule(prim)
584616

585617

586618

@@ -1250,7 +1282,8 @@ def _scatter_add_terms(d_operand, d_updates):
12501282

12511283
@weakref_lru_cache
12521284
def _jet_jaxpr(
1253-
jaxpr: core.ClosedJaxpr, order: int, primals_and_series_avals, in_tree_def
1285+
jaxpr: core.ClosedJaxpr, order: int, primals_and_series_avals, in_tree_def,
1286+
propagate_effective_order: bool = False,
12541287
) -> tuple[core.ClosedJaxpr, Any]:
12551288
# Create a minimal debug_info since JAX 0.8.0+ requires it and validates arg_names count.
12561289
# We can't use the original jaxpr's debug_info because the number of arguments changes
@@ -1262,32 +1295,59 @@ def _jet_jaxpr(
12621295
result_paths=None,
12631296
)
12641297
f = lu.wrap_init(core.jaxpr_as_fun(jaxpr), debug_info=debug_info)
1265-
# Recursive jet calls don't propagate effective_order (inner jit boundaries)
1266-
f_jet, out_tree_def = traceable(jet_fun(jet_subtrace(f), order, None), in_tree_def)
1298+
1299+
if propagate_effective_order:
1300+
# primals_and_series_avals's last entry is the effective_order scalar.
1301+
# Pull it out of the input list and feed it into the inner JetTrace,
1302+
# so the hint survives the jit boundary.
1303+
f_jet, out_tree_def = traceable_with_eff(
1304+
jet_fun_with_eff(jet_subtrace(f), order),
1305+
in_tree_def,
1306+
)
1307+
else:
1308+
# No propagation: inner JetTrace gets effective_order=None.
1309+
f_jet, out_tree_def = traceable(
1310+
jet_fun(jet_subtrace(f), order, None),
1311+
in_tree_def,
1312+
)
12671313
jaxpr_jet, _, consts = pe.trace_to_jaxpr_dynamic(f_jet, primals_and_series_avals)
12681314
return core.ClosedJaxpr(jaxpr_jet, consts), out_tree_def
12691315

12701316

12711317
def _pjit_jet_rule(primals_in, series_in, **params):
1318+
eff = params.get("_jet_effective_order", None)
1319+
propagate = eff is not None
1320+
12721321
primals_and_series, in_tree_def = tree_flatten((primals_in, series_in))
12731322

12741323
# FIX: Get order from enclosing jet trace if series_in is empty
12751324
# When a pjit has no inputs, we need to inherit the order from the enclosing jet trace
12761325
# to ensure consistent series dimensions throughout the computation.
12771326
if len(series_in) == 0:
12781327
# Use the order passed from the enclosing jet trace
1279-
order = params.pop("_jet_order", 0)
1328+
order = params.get("_jet_order", 0)
12801329
else:
12811330
order = series_in[0].shape[0]
12821331

1332+
# When propagating, append effective_order as an extra runtime input so
1333+
# the inner JetTrace can read it. Otherwise the hint dies at this jit
1334+
# boundary.
1335+
if propagate:
1336+
eff_value = jnp.asarray(eff, dtype=jnp.int32)
1337+
bound_inputs = list(primals_and_series) + [eff_value]
1338+
else:
1339+
bound_inputs = list(primals_and_series)
1340+
12831341
primals_and_series_avals = tuple(
1284-
core.shaped_abstractify(x) for x in primals_and_series
1342+
core.shaped_abstractify(x) for x in bound_inputs
12851343
)
12861344
jaxpr_jet, out_tree_def = _jet_jaxpr(
1287-
params["jaxpr"], order, primals_and_series_avals, in_tree_def
1345+
params["jaxpr"], order, primals_and_series_avals, in_tree_def,
1346+
propagate_effective_order=propagate,
12881347
)
12891348
num_series_in = len(primals_in)
12901349
num_series_out = len(params["out_shardings"])
1350+
num_extra = 1 if propagate else 0
12911351

12921352
# Remove internal jet keys from params before passing to pjit.bind
12931353
_jet_keys = {"_jet_order", "_jet_effective_order"}
@@ -1297,16 +1357,17 @@ def _pjit_jet_rule(primals_in, series_in, **params):
12971357
**params_for_pjit,
12981358
"jaxpr": jaxpr_jet,
12991359
"in_shardings": (
1300-
params["in_shardings"] + (sharding_impls.UNSPECIFIED,) * num_series_in
1360+
params["in_shardings"]
1361+
+ (sharding_impls.UNSPECIFIED,) * (num_series_in + num_extra)
13011362
),
13021363
"out_shardings": (
13031364
params["out_shardings"] + (sharding_impls.UNSPECIFIED,) * num_series_out
13041365
),
1305-
"in_layouts": params["in_layouts"] + (None,) * num_series_in,
1366+
"in_layouts": params["in_layouts"] + (None,) * (num_series_in + num_extra),
13061367
"out_layouts": params["out_layouts"] + (None,) * num_series_out,
1307-
"donated_invars": params["donated_invars"] + (False,) * num_series_in,
1368+
"donated_invars": params["donated_invars"] + (False,) * (num_series_in + num_extra),
13081369
}
1309-
result = jex.core.primitives.jit_p.bind(*primals_and_series, **new_params)
1370+
result = jex.core.primitives.jit_p.bind(*bound_inputs, **new_params)
13101371
return tree_unflatten(out_tree_def(), result)
13111372

13121373

‎tests/test_effective_order.py‎

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,72 @@ def test_effective_order_full_K_equals_no_hint():
166166
_, s_no = jet(fn, (x0,), (series_in,))
167167
_, s_K = jet(fn, (x0,), (series_in,), effective_order=jnp.array(K))
168168
np.testing.assert_allclose(s_no, s_K, rtol=1e-12, atol=1e-14)
169+
170+
171+
# ----------------------------------------------------------------------------
172+
# Cross-jit propagation: effective_order must survive jit boundaries.
173+
# Regression coverage for the _pjit_jet_rule fix that threads eff as an
174+
# extra input to the inner pjit.
175+
# ----------------------------------------------------------------------------
176+
177+
178+
@pytest.mark.parametrize("name,fn_factory", [
179+
# Each function uses jnp.* (which wraps in implicit jit) AND composes
180+
# with jnp.exp so the high-order Taylor coefficients are non-zero —
181+
# this lets us verify truncation by comparing the tail.
182+
("jnp.exp", lambda: lambda x: jnp.exp(x)),
183+
("jnp.sin", lambda: lambda x: jnp.exp(jnp.sin(x))),
184+
("jnp.tanh", lambda: lambda x: jnp.exp(jnp.tanh(x))),
185+
("x**0.5", lambda: lambda x: jnp.exp(x ** 0.5)),
186+
("x*x", lambda: lambda x: jnp.exp(x * x)),
187+
("x**3", lambda: lambda x: jnp.exp(x ** 3)),
188+
("compose_two", lambda: lambda x: jnp.exp(jnp.sin(jnp.tanh(x)))),
189+
])
190+
def test_effective_order_propagates_across_implicit_jit(name, fn_factory):
191+
"""Functions that use jnp.* operators (which wrap each call in jit)
192+
still skip work above effective_order. Without the _pjit_jet_rule fix,
193+
the inner JetTrace would see effective_order=None and produce a
194+
full-K series; with the fix, the eff is threaded as an extra input
195+
and the series is truncated."""
196+
fn = fn_factory()
197+
x0 = jnp.asarray(0.5, dtype=jnp.float64)
198+
series_in = jnp.zeros(K, dtype=jnp.float64).at[0].set(1.0)
199+
200+
_, s_ref = jet(fn, (x0,), (series_in,))
201+
_, s_eff = jet(fn, (x0,), (series_in,), effective_order=jnp.array(3))
202+
203+
# Prefix correctness:
204+
np.testing.assert_allclose(
205+
s_eff[:3], s_ref[:3], rtol=RTOL, atol=ATOL,
206+
err_msg=f"{name}: prefix mismatch",
207+
)
208+
# Tail must be truncated (zero), not full. Outer jnp.exp ensures the
209+
# reference tail is non-zero, so this is a real distinguishing test.
210+
assert not jnp.allclose(s_ref[3:], s_eff[3:], rtol=RTOL, atol=ATOL), (
211+
f"{name}: effective_order did not actually skip work — tail equals "
212+
f"the no-hint output, indicating the hint was lost across a jit "
213+
f"boundary. ref={s_ref[3:]}, eff={s_eff[3:]}"
214+
)
215+
# Truncated entries should be exactly zero (the _jet_scan no-op path
216+
# leaves them at the JetTrace's initial zero).
217+
np.testing.assert_allclose(s_eff[3:], 0.0, atol=1e-12,
218+
err_msg=f"{name}: truncated tail not zero")
219+
220+
221+
def test_effective_order_propagates_across_explicit_jit():
222+
"""jax.jit-wrapped subroutines also propagate effective_order."""
223+
@jax.jit
224+
def inner(x):
225+
return jnp.exp(jnp.sin(x))
226+
227+
fn = lambda x: inner(x) + jnp.exp(x)
228+
x0 = jnp.asarray(0.5, dtype=jnp.float64)
229+
series_in = jnp.zeros(K, dtype=jnp.float64).at[0].set(1.0)
230+
231+
_, s_ref = jet(fn, (x0,), (series_in,))
232+
_, s_eff = jet(fn, (x0,), (series_in,), effective_order=jnp.array(3))
233+
234+
np.testing.assert_allclose(s_eff[:3], s_ref[:3], rtol=RTOL, atol=ATOL)
235+
assert not jnp.allclose(s_ref[3:], s_eff[3:], rtol=RTOL, atol=ATOL), (
236+
"effective_order did not propagate through jax.jit"
237+
)

0 commit comments

Comments
 (0)