@@ -937,7 +937,10 @@ def _logistic_taylor(primals_in, series_in, _jet_effective_order=None, **_):
937937
938938 u = _prepend_primal (x , series ) # shape (K+1, ...)
939939 j_idx = jnp .arange (order + 1 , dtype = dtype ) # 0…K
940- u_scaled = j_idx * u # j·u_j
940+ # Reshape for explicit broadcast over leading series axis when x has
941+ # rank >= 1; bare `j_idx * u` fails rank-promotion under jit.
942+ j_bcast = j_idx .reshape ((order + 1 ,) + (1 ,) * x .ndim )
943+ u_scaled = j_bcast * u # j·u_j
941944
942945 pad = (order - 1 , 0 ) # left padding
943946 u_pad = jnp .pad (u_scaled , (pad ,) + ((0 , 0 ),) * (x .ndim ), mode = "constant" )[::- 1 ]
@@ -1475,3 +1478,165 @@ def _register_pjit_log_rule():
14751478 from .log_space import log_space_rules
14761479 log_space_rules [jex .core .primitives .jit_p ] = _pjit_jet_rule
14771480_register_pjit_log_rule ()
1481+
1482+
1483+ def _scan_jet_rule (primals_in , series_in , * , jaxpr , num_consts , num_carry , length ,
1484+ reverse , unroll , linear = None , _split_transpose = None , ** params ):
1485+ """Jet rule for ``lax.scan_p``.
1486+
1487+ Threads Taylor-series tangents through scan iterations. The original
1488+ scan body has signature
1489+
1490+ body(*consts, *carry, *x_per_iter) -> (*new_carry, *y_per_iter)
1491+
1492+ Under jet, every input/output gains a Taylor-series companion of shape
1493+ ``(order, *primal_shape)``. We build a *new* body whose flat input
1494+ layout is
1495+
1496+ [consts_p..., consts_s..., carry_p..., carry_s..., x_p..., x_s...]
1497+
1498+ so that ``lax.scan_p`` can treat consts_p+consts_s as consts, carry_p+
1499+ carry_s as the threaded carry, and x_p+x_s as scan inputs. The body
1500+ is then traced under JetTrace to give it Taylor-aware semantics.
1501+
1502+ Subtlety on xs series shape: outside the scan, an x with primal shape
1503+ ``(T, *Sx)`` carries series of shape ``(order, T, *Sx)`` because the
1504+ JetTrace prepends ``order`` as the leading axis. Inside the scan body
1505+ (per iteration) the body sees ``x_p`` of shape ``(*Sx)`` and expects
1506+ ``x_s`` of shape ``(order, *Sx)`` — so we must transpose xs series to
1507+ ``(T, order, *Sx)`` before binding ``scan_p``, then transpose ys series
1508+ back from ``(T, order, *Sy)`` to ``(order, T, *Sy)`` after.
1509+ """
1510+ if not series_in :
1511+ primals_out = lax .scan_p .bind (
1512+ * primals_in , jaxpr = jaxpr , num_consts = num_consts ,
1513+ num_carry = num_carry , length = length , reverse = reverse ,
1514+ unroll = unroll , linear = linear ,
1515+ _split_transpose = _split_transpose ,
1516+ )
1517+ return primals_out , [zero_series ] * len (primals_out )
1518+
1519+ s0 = series_in [0 ]
1520+ if hasattr (s0 , "sign" ) and hasattr (s0 , "log_mag" ):
1521+ raise NotImplementedError (
1522+ "scan jet rule does not yet support log_space mode "
1523+ "(LogSeries through scan body); use raw mode for now"
1524+ )
1525+ order = s0 .shape [0 ]
1526+ log_space = False
1527+
1528+ consts_p = list (primals_in [:num_consts ])
1529+ init_p = list (primals_in [num_consts :num_consts + num_carry ])
1530+ xs_p = list (primals_in [num_consts + num_carry :])
1531+ consts_s = list (series_in [:num_consts ])
1532+ init_s = list (series_in [num_consts :num_consts + num_carry ])
1533+ xs_s = list (series_in [num_consts + num_carry :])
1534+
1535+ nc , ni , nx = num_consts , num_carry , len (xs_p )
1536+ ny = len (jaxpr .out_avals ) - num_carry
1537+
1538+ body_in_avals = list (jaxpr .in_avals )
1539+ consts_avals_p = body_in_avals [:nc ]
1540+ carry_avals_p = body_in_avals [nc :nc + ni ]
1541+ x_avals_p = body_in_avals [nc + ni :]
1542+ consts_avals_s = [
1543+ core .ShapedArray ((order ,) + a .shape , a .dtype ) for a in consts_avals_p
1544+ ]
1545+ carry_avals_s = [
1546+ core .ShapedArray ((order ,) + a .shape , a .dtype ) for a in carry_avals_p
1547+ ]
1548+ x_avals_s = [
1549+ core .ShapedArray ((order ,) + a .shape , a .dtype ) for a in x_avals_p
1550+ ]
1551+ new_in_avals = (
1552+ consts_avals_p + consts_avals_s
1553+ + carry_avals_p + carry_avals_s
1554+ + x_avals_p + x_avals_s
1555+ )
1556+
1557+ body_fn = core .jaxpr_as_fun (jaxpr )
1558+
1559+ def new_body_fn (* flat_inputs ):
1560+ c_p = flat_inputs [:nc ]
1561+ c_s = flat_inputs [nc :2 * nc ]
1562+ cy_p = flat_inputs [2 * nc :2 * nc + ni ]
1563+ cy_s = flat_inputs [2 * nc + ni :2 * nc + 2 * ni ]
1564+ x_p = flat_inputs [2 * nc + 2 * ni :2 * nc + 2 * ni + nx ]
1565+ x_s = flat_inputs [2 * nc + 2 * ni + nx :]
1566+
1567+ primals_body = list (c_p ) + list (cy_p ) + list (x_p )
1568+ series_body = list (c_s ) + list (cy_s ) + list (x_s )
1569+
1570+ body_wrapped = lu .wrap_init (
1571+ body_fn ,
1572+ debug_info = lu .DebugInfo (
1573+ traced_for = "scan_jet_inner" ,
1574+ func_src_info = "scan body" ,
1575+ arg_names = None , result_paths = None ,
1576+ ),
1577+ )
1578+ f_jet = jet_fun (jet_subtrace (body_wrapped ), order , None , log_space )
1579+ out_p , out_s = f_jet .call_wrapped (primals_body , series_body )
1580+
1581+ new_carry_p = list (out_p [:ni ])
1582+ new_carry_s = list (out_s [:ni ])
1583+ y_p = list (out_p [ni :])
1584+ y_s = list (out_s [ni :])
1585+ return tuple (new_carry_p ) + tuple (new_carry_s ) + tuple (y_p ) + tuple (y_s )
1586+
1587+ new_body_wrapped = lu .wrap_init (
1588+ new_body_fn ,
1589+ debug_info = lu .DebugInfo (
1590+ traced_for = "scan_jet_outer" ,
1591+ func_src_info = "jet scan wrapper" ,
1592+ arg_names = None , result_paths = None ,
1593+ ),
1594+ )
1595+ new_jaxpr_inner , _ , new_consts = pe .trace_to_jaxpr_dynamic (
1596+ new_body_wrapped , tuple (new_in_avals )
1597+ )
1598+ new_jaxpr = core .ClosedJaxpr (new_jaxpr_inner , new_consts )
1599+
1600+ xs_s_for_scan = [jnp .swapaxes (s , 0 , 1 ) for s in xs_s ]
1601+ new_inputs = (
1602+ consts_p + consts_s
1603+ + init_p + init_s
1604+ + xs_p + xs_s_for_scan
1605+ )
1606+
1607+ bind_kwargs = dict (
1608+ jaxpr = new_jaxpr ,
1609+ num_consts = 2 * nc ,
1610+ num_carry = 2 * ni ,
1611+ length = length ,
1612+ reverse = reverse ,
1613+ unroll = unroll ,
1614+ )
1615+ # `linear` and `_split_transpose` exist in older JAX (≤0.9) scan_p but
1616+ # were removed in newer versions. Pass through only when present.
1617+ if linear is not None :
1618+ consts_linear = list (linear [:nc ])
1619+ init_linear = list (linear [nc :nc + ni ])
1620+ xs_linear = list (linear [nc + ni :])
1621+ bind_kwargs ["linear" ] = tuple (
1622+ consts_linear + [False ] * nc
1623+ + init_linear + [False ] * ni
1624+ + xs_linear + [False ] * nx
1625+ )
1626+ if _split_transpose is not None :
1627+ bind_kwargs ["_split_transpose" ] = _split_transpose
1628+
1629+ out_flat = lax .scan_p .bind (* new_inputs , ** bind_kwargs )
1630+
1631+ new_carry_p = list (out_flat [:ni ])
1632+ new_carry_s = list (out_flat [ni :2 * ni ])
1633+ ys_p = list (out_flat [2 * ni :2 * ni + ny ])
1634+ ys_s = list (out_flat [2 * ni + ny :])
1635+ ys_s = [jnp .swapaxes (s , 0 , 1 ) for s in ys_s ]
1636+
1637+ primals_out = new_carry_p + ys_p
1638+ series_out = new_carry_s + ys_s
1639+ return primals_out , series_out
1640+
1641+
1642+ jet_rules [lax .scan_p ] = _scan_jet_rule
0 commit comments