Changelog
[0.1.0] - 2026-08-08
This release brings a large number of layers, utilities and data structures in line
with torch_geometric semantics. Numerical outputs, module state layouts and a
few public signatures change as a result, so upgrading from 0.0.4 requires the
migrations listed below.
Breaking Changes – Dependencies
Minimum dependency versions are raised to
flax>=0.12.8,jax>=0.10.0,jaxlib>=0.10.0andnumpy>=2.0. The previous floors were not installable as declared:flaxonly publishes a lower bound onjax, soflax==0.12.0would resolve against a currentjaxand then fail at import.flax>=0.12.1is the first release providingnnx.Variable.get_value()/set_value(), which this library needs for variables that may holdNone; the floor is set at the version CI actually exercises.jax0.10 in turn requiresnumpy>=2.0, so the oldnumpy>=1.21was never satisfiable alongside it.A
test-minimum-versionsCI job installs these exact lower bounds and runs the full suite against them, so the declared floor cannot drift from the tested one.
Breaking Changes – Public API
BatchNormandLayerNormno longer accept the arguments they never read:axis,axis_name,axis_index_groupsanduse_fast_varianceonBatchNorm;reduction_axes,feature_axes,axis_name,axis_index_groupsanduse_fast_varianceonLayerNorm. All of these were stored and silently ignored – in particularaxis_name, which read as if it synchronized statistics across devices. Passing one now raisesTypeError.rngsis a required keyword-only argument of every layer that owns parameters:GCNConv,GATConv,GATv2Conv,SAGEConv,TransformerConv,MLP,BasicGNNand itsGCN/GAT/GraphSAGE/GINspecialisations, andBasicGNN.init_conv. Every one of these already failed without it, with an unhelpfulAttributeError: 'NoneType' object has no attribute 'params'raised from inside layer construction; the failure is now aTypeErrorat the call site naming the missing argument.TransformerConvadditionally used to fall back to a fixednnx.Rngs(0), so two layers built withoutrngssilently shared an initialization – that fallback is gone.JumpingKnowledge(mode="lstm")raisesValueErrorwhennum_features,num_layersorrngsis missing, instead ofAssertionError(which vanishes underpython -O) or, forrngs, anAttributeError. Layers whose parameters do not need a key –BatchNorm,LayerNorm,GraphNorm,GINConv,EdgeConv, andJumpingKnowledgeincat/maxmode – still accept none.TransformerConv.messageis renamed to_attention_message. It never implemented theMessagePassing.message()contract: it takes projected query/key/value tensors and is not reached throughpropagate.GCNConv(cached=True)no longer fills its cache on the first forward pass. Callconv.precompute_norm(edge_index, edge_weight=None, num_nodes=None)once, outside of any JAX transformation, before the first forward pass; otherwise the layer raises aRuntimeError. Useconv.reset_cache()before re-runningprecompute_normfor a different graph, or keepcached=Falseto normalize on every call.MessagePassing.propagatefollows the PyG bipartite convention:xis either a single feature table or an(x_src, x_dst)tuple, wherex_srcholds the source set andx_dstthe target set. The output has one row per target node. Passing a tuple whose sizes disagree with an explicitsizeraises aValueError.MessagePassing.messageis invoked exactly once per forward pass.MessagePassing.message_and_aggregateis now an opt-in fused hook: the base class raisesNotImplementedError, andpropagatedispatches to it only for subclasses that override it. Its first argument is the node feature table (or bipartite tuple), not the pre-computed messages.Batchis a valid JAX pytree. Its batching configuration –NODE_INDEX_FIELDS,ELEMENT_LEVEL_FIELDS,GRAPH_LEVEL_FIELDSand_DATA_CLASS– areClassVarclass attributes rather than dataclass fields, so they no longer appear inBatch.__init__, indataclasses.fields(Batch)or in the pytree. ReplaceBatch(x=..., NODE_INDEX_FIELDS={'face'})with aBatchsubclass that declaresNODE_INDEX_FIELDS: ClassVar[set[str]] = {'face'}in its class body.to_edge_indexalways returns an edge attribute array; it never returnsNonefor the second element.to_undirectedcoalesces its result: the returned edge list is row-wise sorted, duplicated edges appear once, and their features are merged withreduce. The number of edges is therefore data-dependent and the function cannot be traced byjax.jit.coalesceandto_undirectedacceptreduce="sum"as an alias ofreduce="add".scatter, on the other hand, rejects unknown reductions with an explicitValueError; only"add"/"sum","mean","max"and"min"are supported ("mul"was never implemented).TopKPooling/SAGPoolinginterpretratioby type, matching PyG: afloatkeeps \(\lceil \mathrm{ratio} \cdot N_i \rceil\) nodes per graph and anintkeeps exactly that many.ratio=2.0previously kept two nodes and now keeps every node; writeratio=2for the old behavior.Pooling is explicit about traceability.
global_add_pool,global_mean_pool,global_max_poolandglobal_min_poolraise aValueErrorwhenbatchis a tracer andsizeis omitted – passsize=<num_graphs>insidejax.jitorjax.vmap.TopKPoolingandSAGPoolingselect a data-dependent number of nodes and cannot be traced at all.LayerNorm(mode="graph")andGraphNormlikewise need an explicitbatch_sizeunder a trace.
Breaking Changes – Module State Layout
Checkpoints written by 0.0.4 are not loadable as-is:
GCNConvbuilds its innerLinearwithuse_bias=Falseand holds its own bias, which is added after aggregation. The state key moves fromlinear.biastobias.GraphNormgains amean_scaleparameter, initialized to ones.BatchNormstoresrunning_mean,running_varandnum_batches_trackedasnnx.BatchStatinstead ofnnx.Variable, sonnx.splitandnnx.state(..., nnx.Param)partition them differently.Layers that support dropout always hold a
Dropoutsubmodule; a rate of0.0makes it return its input untouched without drawing a key, rather than the layer holdingNone. Every such module therefore carries dropoutRngStateeven at rate 0, sojax.gradover an unfilterednnx.split(model)now fails on the integer RNG counters. Split the parameters out first:graphdef, params, rest = nnx.split(model, nnx.Param, ...)
Parameter initialization is unchanged: the
Dropoutis constructed after the layers that draw parameter keys.
Breaking Changes – Numerics
Trained weights still load (subject to the state-layout notes above), but outputs move:
GATConv/GATv2Convnormalize attention coefficients per head. Outputs change forheads > 1.TransformerConvadds the projected edge features to the keys as well as to the values, so edge information conditions the attention scores.GCNConvnormalizes by weighted degree (rather than by edge count) and adds its bias after aggregation.GraphNormcomputes per-feature, per-graph statistics and applies the learnablemean_scale.LayerNorm(mode="graph")reduces over both the node axis and the feature axis for each graph, making it genuinely distinct frommode="node".BatchNormpools statistics over every node of the mini-batch and ignores thebatchvector, which it previously used to average per-graph statistics. Its running variance now tracks the unbiased estimator, matching PyTorch. UseGraphNormwhen per-graph statistics are wanted.TopKPooling/SAGPoolingalways apply the score gate to the pooled features, so the scoring projection receives gradient.multiplieris applied after the gate and does not influence node selection.JumpingKnowledge(mode="lstm")computes the correct bidirectional GRU recurrence, so its outputs change.scatter_stdapplies Bessel’s correction by default, matchingtorch_scatter. Passunbiased=Falsefor the previous population standard deviation.scatter_max/scatter_minpreserve integer input dtypes instead of promoting to float.GCNConvmatchestorch_geometricon graphs that already contain self-loops: an existing loop keeps its own weight and is counted once in the degree instead of being duplicated alongside an injected unit loop. To keep the output shape static and therefore traceable, the duplicated row remains in the returnededge_indexwith weight0.0; every coefficient and the convolution output agree with PyG.TopKPooling(min_score=...)thresholds and gates with the softmax of the unnormalized projection \(Xp\), matching PyG’sSelectTopK. The projection was previously divided by \(\lVert p \rVert\) first, which rescaled the logits and collapsed the gate to nearly one-hot at initialization. Theratiopath is unchanged and still normalizes by \(\lVert p \rVert\).SAGEConvapplies its neighbor transform after aggregation, as \(\mathbf{W}_2 \cdot \mathrm{aggr}_j \mathbf{x}_j\). Onlyaggr="max"changes: an elementwise maximum does not commute with a linear map, so the previous ordering computed \(\max_j (\mathbf{W}_2 \mathbf{x}_j)\) – a maximum taken in the output space, mixing columns drawn from different source nodes.aggr="mean"/"gcn"are unaffected, since sum and mean do commute.DynamicEdgeConvbuilds the k-NNedge_indexwith the neighbor as source and the querying node as target, so a node aggregates over the neighbors it selected. The rows were previously the other way round, which built the reverse k-NN graph: because “j is among i’s k nearest” is not symmetric, every node aggregated over the nodes that had selected it, and a node selected by nobody received no messages at all and max-aggregated to a zero row. Every ported DGCNN model changes.GATConv/GATv2Convno longer count a pre-existing self-loop twice. PyG removes self-loops before inserting its own; without that removal a node arriving with a loop got two, roughly doubling its self-attention mass and correspondingly down-weighting its real neighbors. Dropping the duplicate column would make the edge count data-dependent and breakjax.jit, so its attention logit is driven to \(-\infty\) instead, which is exactly a softmax weight of zero. The duplicate row stays in theedge_indexreturned byreturn_attention_weights=True, carrying a weight of zero. With a stringfill_valuethe generated loop features still reduce over a set that includes the original loop, which PyG excludes.GATConv/GATv2Convsize their self-loop set frommin(num_src_nodes, num_dst_nodes)on a bipartite graph, matching PyG: a self-loop only exists for a node present in both endpoint tables. Sizing it from the target count alone appended loops whose source index was out of range, and JAX’s array indexing clamps such an out-of-bounds gather to the last row rather than raising – several target nodes received the same fabricated message, and a target with no incoming edge acquired a value. Both layers now also validate their gather indices and raiseIndexError, whichpropagatealready did for the layers that route through it.scatter_mean,scatter_std,segment_mean,GraphNormandLayerNorm(mode="graph")accumulate both their running total and their member count in at least float32, never in a narrower input dtype. bfloat16 carries 8 mantissa bits, so its consecutive integers stop at 256 –256 + 1rounds back to 256 – and both accumulators froze partway through any segment larger than that, leaving the quotient wrong by a degree-dependent factor. A floating-point caller’s dtype is still what comes back; integer inputs divide to float32, asjax.numpy.true_divide()would.scatter_softmax,scatter_log_softmaxandscatter_logsumexpaccumulate their per-group exponential sums in at least float32 as well. In bfloat16 the running normalizer froze at 256, so the attention weights of any larger group – these functions normalizeGATConvandTransformerConvattention – summed to substantially more than 1.batch_histogramassigns each value to the bin whose interval contains it, matchingnumpy.histogram(): with an explicitmin_val/max_valthe values outside the range are dropped, not folded into the edge bins. It previously searched only the left edges and from the left, which pushed every value strictly inside a bin one place to the right, left bin 0 collecting only values exactly equal to the lower bound, and made the last bin absorb the overflow. One residual divergence: bin edges are computed in the working precision (float32), so a value exactly on an interior edge can land one bin away from numpy’s float64 edges.TransformerConvmatches PyG’s parameterization: the output projection that PyG does not have is removed (the forward now ends at the skip/beta combination, as the docstring formula always claimed), the fused query/key/value projection carries a bias like PyG’s threeLinearlayers, heads are concatenated or averaged before the skip term, andlin_skip/lin_betaare sized for the final output width underconcat=False.beta=Truenow requiresroot_weight=Trueand is ignored otherwise – gating against the node’s raw value projection mixed a transformed root feature into every row of a layer documented not to have one, and gave an isolated node a nonzero output where PyG yields zero.The
GATmodel concatenateshidden_features // headsnarrow heads on its last layer whenout_featuresisNone, as PyG’sBasicGNNdoes; it previously averaged full-width heads into the same output shape, hiding a different architecture and parameterization. It also forwards itsdropout_rateinto everyGATConv/GATv2Convas attention dropout, the GAT paper’s primary regularizer.The
GCN/GAT/GraphSAGE/GINmodels forward their remaining keyword arguments to the convolution constructors, soGCN(..., bias=False),GIN(..., eps=0.7)andGAT(..., add_self_loops=False)take effect and an unsupported argument raisesTypeError. All of these were silently discarded.GATConv/GATv2Convwith a bipartite(x_src, None)input omit the target attention term, as PyG does. Both layers previously gathered the source table at target indices to fabricate target features, changing every attention weight.GATv2Convdefaultsfill_valueto"mean", matching PyG andGATConv; self-loop edge features were previously zero-filled.global_sort_poolsorts by the last feature channel (the DGCNN SortPooling operator, PyG’sSortAggregation) instead of by the feature sum, and zero-pads small graphs after selection, so padding can no longer outrank real nodes whose sort scores are negative.in_degree/out_degreesize their result by every node the fulledge_indexmentions. Inferring the count from the counted row alone silently truncated the vector whenever a node appeared only at the other endpoint.Data.is_directedcompares the lexicographically sorted edge list with its reverse, making it exact at any graph size and multiset-correct for duplicated edges. The previoussrc * num_nodes + dstpacking wrapped int32 above ~46k nodes – the same overflow fixed incoalesce– and its set semantics called a graph with an unbalanced duplicated edge undirected.add_remaining_self_loopsgives every node exactly one self-loop: existing loops are removed first (collapsing duplicates), the per-node loops are appended in node order, and a replaced loop keeps its attribute. Duplicated input loops were previously all retained, where PyG collapses them.TransformerConv(beta=True)concatenates its gate input as \([\mathbf{m}_i, \mathbf{W}_1 \mathbf{x}_i, \mathbf{m}_i - \mathbf{W}_1 \mathbf{x}_i]\), the order PyG’s implementation uses – PyG’s docstring lists the reversed order, which its code does not. Found by the weight-transplant parity harness: with the previous order a transplantedlin_betacomputed a different gate.global_max_pool/global_min_pool/global_mean_poolkeep the rank of the node features: a 1-D input[num_nodes]pools to[batch_size], and inputs such as[num_nodes, heads, features]pool to[batch_size, heads, features].scatter_log_softmaxreturns-infrather thanNaNfor a group whose entries are all-inf, soexp()of the result matchesscatter_softmax.coalesceaccumulates edge identifiers inint64, fixing silent overflow on graphs with more than roughly 46k nodes.Batch.from_data_listrejects an attribute that is present on only some graphs when that attribute aligns with the batch vector, and accepts one that aligns with the edge or element axis – an edgeless graph may sit alongside graphs carryingedge_attr.Batch.to_data_listpreserves trailing graphs that contain no nodes, and keeps the leading dimension of a graph-levely.SAGEConvaccepts the(x_src, None)bipartite pair again, andEdgeConvandGINConvaccept the(x_src, x_dst)tuples their docstrings advertise.add_self_loopswith a stringfill_valueno longer requiresnum_nodes.Batchsubclasses with severalNODE_INDEX_FIELDSpick their primary index field alphabetically instead of by set iteration order, which varied with the per-process hash seed and made the same legitimate data list collate in one process and raiseRuntimeErrorin another.
Other Changes
A parity suite under
tests/parity/transplants weights layer by layer into an installedtorch_geometricand compares outputs elementwise – convolutions, normalization layers, scatter/loop/graph utilities and poolings. A dedicated CI job installs CPUtorchandtorch_geometricand gates releases on it; the jobs without torch skip the package. The one deliberate divergence (stringfill_valueself-loop features on a graph that already carries a loop) is pinned as a strictxfail.The
docsextra installs on Python 3.13:sphinx==5.1.1andsphinx-autodoc-typehints==1.19.2– neither importable there – are replaced by floors on current releases, and the documentation builds warning-free against sphinx 9.The sdist no longer ships
tests/test_version.py, which setuptools’ legacy default template included on its own. It was the only test file in the archive and could not pass from an sdist install, since it reads the changelog out of the excludeddocs/tree.New
GINEConvlayer –GINConvwith edge features fused into every message, from “Strategies for Pre-training Graph Neural Networks” – contributed by @jiinyih. Edge features of a different width than the nodes are projected viaedge_dim; withoutedge_dima width mismatch raisesValueError, as in PyG, rather than silently broadcasting. Supports bipartite(x_src, x_dst)input.New
parse_dtype()utility resolving a dtype spec – a plain or prefixed string ("float32","jnp.bfloat16","np.int32"), a scalar type, or a dtype object – to the matching jax.numpy scalar type. Every jraphxdtypeargument (degree/in_degree/out_degree,GCNConvnormalization,BatchNorm/LayerNormdtype/param_dtype) now routes through it, so a dtype can come straight from a configuration file and an invalid spec fails at the call site with a clear error. Abstract categories such as"floating"are rejected there rather than surfacing later at the first array construction.Type annotations use
jax.Array, the canonical public name of the array type, instead of thejnp.ndarrayalias – in the library, the tests and the documentation. No runtime behavior changes.mypy src/passes under the project’s strict configuration and is a required CI check. It previously had no execution path at all – no CI job, notypecheckMake rule – and reported 118 errors, among them the missing-rngscrashes above.GCNgainsprecompute_norm(edge_index, edge_weight=None, num_nodes=None), which fills the cache of everyGCNConvlayer at once. It must be called eagerly before the first forward pass of aGCN(cached=True).Passing
edge_weightto aBasicGNNwhosesupports_edge_weightisFalse(oredge_attrwhensupports_edge_attrisFalse) raises aValueErrorinstead of silently dropping the argument. CustomBasicGNNsubclasses that forward edge information must set the corresponding class attribute.The
batch_sizeargument ofBasicGNN.__call__(and thereforeGCN,GAT,GraphSAGE,GIN) and ofMLP.__call__is forwarded to thelayer_normandgraph_normlayers instead of being ignored. Supply it as a Pythonintwhen such a model is traced together with abatchvector.The deprecated Flax
.valueaccessor is gone throughout the library. Usevariable[...]for array-valuednnx.Variableobjects andvariable.get_value()/variable.set_value(x)for variables that may holdNone.A
NOTICEfile collects the third-party notices the project’s own README already claimed, including the MIT permission notice for the PyTorch Geometric code and docstrings this library derives from. It is listed inlicense-files, so it ships in both the wheel and the sdist.Documentation corrections. Three pages taught a training step wrapped in
jax.jit(), where the parameter update is traced on a copy of the module state and silently discarded – no error, a plausible loss, and not one parameter moved. They now usennx.jit(), and JAX JIT Compilation explains when each is appropriate – including thennx.split()/nnx.merge()functional training loop, under whichjax.jit()is correct because the state is threaded explicitly, and which the Flax performance guide recommends for hot loops. TheMessagePassingprose described PyG’spropagate(**kwargs)and_i/_jargument lifting, neither of which JraphX implements:messageis dispatched positionally, so a signature writtenmessage(self, x_i, x_j)binds the source features tox_i. Two shipped examples did exactly that. Also fixed: the twoBatch-subclass recipes, which listed node-level fields asELEMENT_LEVEL_FIELDSand raised; aBatchNorm(affine=True)snippet, since the argument is spelleduse_scale/use_bias; a reference to ajraphx.data.vmap_batchmodule that does not exist; and a call to the removedjax.tree_map.Example corrections.
examples/gcn_jraphx.pyandexamples/gcn_standalone.pysharded one graph’s nodes against a slice of its edges undershard_map, so edges carrying global node ids gathered out of range and every message crossing a device boundary was dropped – silently, producing plausible but wrong losses and accuracies. Both now shard whole graphs, and the sharded result matches the unsharded one exactly.examples/nnx_transforms.pypassed its model and optimizer tonnx.scanas broadcast inputs, whose mutations Flax discards, so its “memory-efficient training” never updated a parameter; it now steps through the mini-batches and asserts that the loss falls.A third documentation pass, executing every snippet it touched. The flagship Introduction by Example training and evaluation examples boolean-indexed traced arrays under
nnx.jit()and crashed on their first call; they now weight the per-node loss by the mask. jraphx.nn.pool claimedTopKPoolingis JIT-compatible directly above the note explaining why it is not; the section now pools eagerly and jits the dense computation after it. Also fixed: ajraphx.data.DataLoaderimport that does not exist, aDeepGNNexample calling itsData-taking model with two arrays, a vmap snippet feeding 16-feature graphs to the page’s 2-feature model, four constructor calls missing the requiredrngs, a snippet usingjax.randomwithout importing it, and docstrings advertising anaggr="lstm"that raisesNotImplementedError, a bipartiteTransformerConvinput the code cannot accept, aTopKPoolingformula applying its score nonlinearity twice, and aJumpingKnowledge“bi-directional LSTM” that is implemented as two GRU cells.
[0.0.4] - 2025-10-30
Breaking Changes
Updated minimum Flax requirement to 0.12.0 for improved pytree handling:
Now uses
nnx.Listfor module lists
Now uses
nnx.data(None)for optional module attributes
[0.0.3] - 2025-09-04
Initial release of JraphX.
Features
Core Data Structures
Dataclass: Single graph representation with node features, edge indices, edge attributes, and graph-level propertiesBatchclass: Efficient batching of multiple graphs into disconnected graph batches with automatic indexing management
Message Passing Framework
Unified
MessagePassingbase class providing a standardized interface for all graph neural network layersFlexible message computation, aggregation (sum, mean, max, min), and node update functions
Support for both node-to-node and edge-enhanced message passing paradigms
Graph Convolution Layers
GCNConv: Graph Convolutional Network with spectral-based convolution and optional edge weightsGATConv: Graph Attention Network with multi-head attention mechanism and learnable attention weightsGATv2Conv: Improved Graph Attention Network with enhanced attention computation for better expressivityGraphSAGE(SAGEConv): GraphSAGE with multiple aggregation functions (mean, max, LSTM) for inductive learningGINConv: Graph Isomorphism Network with theoretical guarantees for graph representation powerEdgeConv: Dynamic edge convolution for learning on point clouds and dynamic graph constructionDynamicEdgeConv: Enhanced EdgeConv with k-nearest neighbor graph constructionTransformerConv: Graph Transformer layer with optimized query-key-value projections and positional encodings
Pooling Operations
Global pooling:
global_add_pool,global_mean_pool,global_max_pool,global_min_poolfor graph-level representationsAdvanced pooling:
global_softmax_pool,global_sort_poolfor differentiable and sorted aggregationsHierarchical pooling:
TopKPoolingandSAGPoolingfor coarsening graph structures with learnable node selectionBatched operations: Optimized versions (
batched_global_*_pool) for efficient parallel processing of graph batches
Utility Functions
Scatter operations: Comprehensive set including
scatter_add,scatter_mean,scatter_max,scatter_min,scatter_std,scatter_logsumexpfor flexible aggregationScatter softmax:
scatter_softmax,scatter_log_softmax,masked_scatter_softmaxfor attention-like mechanismsGraph utilities: Degree computation (
degree,in_degree,out_degree), self-loop management (add_self_loops,remove_self_loops)Conversion functions:
to_dense_adj,to_edge_index,to_undirectedfor different graph representationsGraph preprocessing:
coalescefor edge deduplication,maybe_num_nodesfor automatic node count inference
Pre-built Models
GCN,GAT,GraphSAGE,GIN: Complete model implementations with configurable depth, hidden dimensions, and activation functionsJumpingKnowledge: Multi-layer aggregation with concatenation, max, and LSTM-based combination strategiesMLP: Multi-layer perceptron with dropout, batch normalization, and flexible activation functionsBasicGNN: Abstract base class for implementing custom GNN architectures with standardized interfaces
Normalization Layers
BatchNorm: Batch normalization with running statistics for stable training across graph batchesLayerNorm: Layer normalization supporting both node-wise and graph-wise normalization schemesGraphNorm: Graph-specific normalization designed for graph neural network architectures
JAX Integration & Performance
Extensive use of
jax.vmapandnnx.vmapfor efficient parallel processing of graph batchesMemory-efficient training patterns using
jax.lax.scanandnnx.scanfor sequential operationsJIT compilation support for all operations with optimized JAX primitives
Efficient scatter operations using JAX’s advanced indexing (
at[].add/max/min) for high-performance aggregation