@@ -11,8 +11,15 @@ def scatter_sum(
1111 dim_size : int | None = None ,
1212 * ,
1313 use_custom_kernel : bool = False ,
14+ jvp_safe : bool = False ,
1415):
15- """Sum rows of ``source`` into output rows selected by one-dimensional ``index``."""
16+ """Sum rows of ``source`` into rows selected by one-dimensional ``index``.
17+
18+ Set ``jvp_safe=True`` for forward-mode differentiation with respect to
19+ ``source``. This path requires eager, fixed indices and implements the
20+ same reduction with a sorted prefix sum because MLX 0.31's indexed-add
21+ primitive has no JVP rule.
22+ """
1623
1724 mx , _ = require_mlx ()
1825 if source .ndim < 1 :
@@ -25,6 +32,10 @@ def scatter_sum(
2532 dim_size = 0 if index .size == 0 else int (mx .max (index ).item ()) + 1
2633 if dim_size < 0 :
2734 raise ValueError ("dim_size must be non-negative" )
35+ if jvp_safe :
36+ if use_custom_kernel :
37+ raise ValueError ("jvp_safe=True requires use_custom_kernel=False" )
38+ return _fixed_index_scatter_sum (source , index , dim_size )
2839 use_metal = (
2940 use_custom_kernel
3041 and mlx_metal_available ()
@@ -51,6 +62,44 @@ def scatter_sum(
5162 return output .at [index ].add (source )
5263
5364
65+ def _fixed_index_scatter_sum (source , index , dim_size : int ):
66+ """Sparse JVP-safe scatter for an eager, fixed index array."""
67+
68+ mx , _ = require_mlx ()
69+ import numpy as np
70+
71+ try :
72+ host_index = np .asarray (index )
73+ except Exception as error :
74+ raise RuntimeError (
75+ "jvp_safe scatter requires an eager fixed index array"
76+ ) from error
77+ if host_index .size :
78+ minimum = int (host_index .min ())
79+ maximum = int (host_index .max ())
80+ if minimum < 0 or maximum >= dim_size :
81+ raise ValueError ("index values must satisfy 0 <= index < dim_size" )
82+
83+ order_host = np .argsort (host_index , kind = "stable" )
84+ sorted_index = host_index [order_host ]
85+ rows = np .arange (dim_size , dtype = host_index .dtype )
86+ starts_host = np .searchsorted (sorted_index , rows , side = "left" )
87+ ends_host = np .searchsorted (sorted_index , rows , side = "right" )
88+ order = mx .array (order_host , dtype = mx .int32 )
89+ starts = mx .array (starts_host , dtype = mx .int32 )
90+ ends = mx .array (ends_host , dtype = mx .int32 )
91+
92+ sorted_source = source [order ]
93+ prefix = mx .concatenate (
94+ (
95+ mx .zeros ((1 , * source .shape [1 :]), dtype = source .dtype ),
96+ mx .cumsum (sorted_source , axis = 0 ),
97+ ),
98+ axis = 0 ,
99+ )
100+ return prefix [ends ] - prefix [starts ]
101+
102+
54103def radius_graph (positions , radius : float , batch = None ):
55104 """Build the directed, loop-free batched radius graph used by gate-points models.
56105
0 commit comments