Library / SDK
rusty1s/pytorch_scatter avatar
rusty1s/pytorch_scatter

PyTorch Scatter: three index layouts, four reductions and a version string that stopped at 2.1.2

PyTorch Extension Library of Optimized Scatter Operations

1,746 stars207 forksPythonMIT

At a glance

What is it?
pytorch_scatter adds the sparse update operations PyTorch itself does not carry, with sorted, unsorted and pointer-based index layouts plus GPU kernels and backward passes. Its weak spot is packaging: the wheel index has moved to PyTorch 2.12 while setup.py still reports 2.1.2.
Who is it for?
Adopt pytorch_scatter when your model reduces neighbour features and you want CPU and GPU kernels, sorted and unsorted paths, and backward passes already written. Do not adopt it if you need a CUDA build on macOS or a version string that tracks your torch install.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 142 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Scatter and segment both reduce on a group-index tensor

PyTorch Scatter fills one specific hole: the sparse update operations that the main PyTorch package does not carry. Everything in the library is a reduce operation, and every reduce operation is driven by one tensor, the group index, that says which group each input row belongs to. Three functions cover the layouts. `scatter` takes that index in arbitrary order. `segment_coo` requires it sorted. `segment_csr` works from pointers that mark group boundaries instead of a group index at all, which is the shape you already hold when neighbours come out of a compressed adjacency structure. All three reduce with `"sum"|"mean"|"min"|"max"`.

The sorted-index requirement is the whole difference between the two families, and it settles your code shape before anyone argues about speed. An unsorted edge list breaks the segment contract; scatter takes the same data unchanged.

Four composite reductions sit on top of the scatter kernels

Beyond the three layouts, four named functions are described as composite: `scatter_std`, `scatter_logsumexp`, `scatter_softmax` and `scatter_log_softmax`. Each is documented as making use of the `scatter_*` operations under the hood, so all four inherit the unsorted-index contract rather than the segment one. That closes a door for anyone reaching for log-sum-exp aggregation over neighbours, since the segment path does not back those calls.

One paragraph covers the shared properties of the whole operation set: the operations are broadcastable, they work on varying data types, and each has a CPU and a GPU implementation with a matching backward. The same paragraph calls them fully traceable, but names no exporter or tracing target for that claim, so treat it as something to check against your own export path.

The reduction that returns an extra tensor shows what the API looks like in practice. In the published example, `scatter_max` hands back both the reduced values and the index positions that won each group, so a min or max call hands back the index position that won each group next to the reduced value.

Picking a wheel: the index is per PyTorch build and per CUDA tag

Binary installation is a URL lookup rather than a resolver. You hand pip an index with a -f flag, and the path you write encodes one exact PyTorch build and one CUDA tag. For a PyTorch 2.12 install:

bash
pip install torch-scatter -f https://data.pyg.org/whl/torch-2.12.0+${CUDA}.html

`${CUDA}` is a placeholder you replace with `cpu`, `cu126`, `cu130` or `cu132` to match your own torch install.

The PyTorch 2.11 line reads the same apart from the tag set:

bash
pip install torch-scatter -f https://data.pyg.org/whl/torch-2.11.0+${CUDA}.html

There the accepted values are `cpu`, `cu126`, `cu128` and `cu130`. PyTorch 2.10 and 2.9 repeat that same four-tag shape, so one command format covers four consecutive torch generations. Only 2.12 changes the set, and it does so by dropping cu128 and adding cu132. Watch the difference: carrying a URL from one section to another points pip at an index with no matching wheel for that build. Older builds go back to PyTorch 1.4.0, and for those the README says to name the supported version explicitly or pass `pip install --no-index`, otherwise pip starts compiling the C++ and CUDA sources on the machine you are sitting at. That index is the upgrade path: a new PyTorch build means a new URL, never a new package name.

macOS wheels are CPU-only, so the GPU kernels never arrive

Each of the four wheel tables marks Linux and Windows across every CUDA tag and marks macOS in the `cpu` column alone. That holds for PyTorch 2.9, 2.10, 2.11 and 2.12 without exception. No macOS row carries a CUDA wheel, and the README does not promise one for any listed version. A developer on a Mac therefore installs the CPU implementation and its backward pass and never touches the GPU path the project advertises.

Source builds on macOS lose something else. In `get_extensions`, the OpenMP branch is guarded by `sys.platform != 'darwin'`, so that compile define is never added there, while the -O3 and -Wno-sign-compare flags still are. Windows takes a third path: on `sys.platform == 'win32'` the build adds a `torchscatter_EXPORTS` define, and the -Wno-sign-compare flag is skipped because `os.name` is nt there. On a Mac carrying an NVIDIA card, nothing in the wheel tables or in those platform conditions accounts for the hardware at all.

Source builds are steered by environment variables

When no wheel fits, the build is steered by environment variables read in `setup.py` rather than by command line flags. `FORCE_CUDA` adds a CUDA target next to the CPU one, while `FORCE_ONLY_CUDA` and `FORCE_ONLY_CPU` narrow the build to a single target:

python
if os.getenv('FORCE_CUDA', '0') == '1':
    suffices = ['cuda', 'cpu']
if os.getenv('FORCE_ONLY_CUDA', '0') == '1':
    suffices = ['cuda']
if os.getenv('FORCE_ONLY_CPU', '0') == '1':
    suffices = ['cpu']

Two more variables change the artefact rather than the target list. Setting `WITH_SYMBOLS` to 1 removes the -s linker flag, which is what you want before pointing a debugger at a custom kernel. `BUILD_DOCS` set to 1 turns on the documentation build.

CUDA is attempted only when `torch.cuda.is_available()` is true and either `CUDA_HOME` is set or `torch.version.hip` is populated, so on a host where the first test is false the target list collapses to CPU and the compile finishes without any device code. One documented escape hatch covers the container case:

bash
export TORCH_CUDA_ARCH_LIST = "6.0 6.1 7.2+PTX 7.5+PTX"

The build backend also lists torch as a build requirement in `pyproject.toml`, so an isolated build pulls torch into the build environment before a single source file is compiled. Sources themselves are globbed from the `csrc/` directory as .cpp files, and any path containing hip is dropped from the list so that generated files from an earlier HIP build cannot come back on a rebuild. `CMakeLists.txt` and `cmake/` sit at the top level for the C++ consumers rather than for the Python install path.

A hard-coded 2.1.2 in setup.py while wheels reach PyTorch 2.12

Here is the part a skim misses. The version constant in `setup.py` is a hard-coded literal:

python
__version__ = '2.1.2'

Nothing in that file derives a version from the torch build being compiled against, and no other file in the tree supplies one. Meanwhile the install tables in the README carry PyTorch 2.12 with cu126, cu130 and cu132. The package calls itself 2.1.2 while publishing binaries for a torch generation several major versions past that string. The README's own instruction for older PyTorch builds, name the supported version explicitly or pass `pip install --no-index`, exists to stop a manual installation from source, and a version string frozen at 2.1.2 is exactly what makes that fallback easy to trigger by leaving the -f flag off. Anyone pinning this in a lock file should read the literal first and decide deliberately, because nothing in the tree reconciles those two numbers.

Three index contracts, and the sort you pay to keep them

The three functions are genuine alternatives to one another, and the difference is structural. When neighbour indices arrive sorted, `segment_coo` consumes them directly. When they arrive in arbitrary order the contract is void, so you either sort the index yourself, paying for a permutation you then have to invert to line results back up, or you call `scatter` and stay on the unsorted path for the whole model. `segment_csr` is the third option: with row pointers already in hand, the pointers define the group boundaries, no group index tensor gets built, and no sort is needed.

That decision propagates further than one layer. Because the four composite functions are built on the `scatter_*` operations, a codebase that standardises on `scatter_softmax` or `scatter_logsumexp` for neighbour aggregation is pinned to the unsorted contract even in the layers where the data happens to arrive sorted. Standardising on `segment_coo` instead buys the sorted path everywhere and puts the sort in your own code where you can see it.

What the tree holds that the README never explains

The top level carries far more than the Python package: `.coveragerc`, `CMakeLists.txt`, `MANIFEST.in`, `benchmark/`, `cmake/`, `csrc/`, `docs/`, `readthedocs.yml`, `setup.cfg`, `setup.py`, `test/` and `torch_scatter/`. Exactly one directory gets a documented command, and it is the short one:

bash
pytest

The `benchmark/` directory has no invocation documented anywhere in the README, so the harness sits in the tree with an entry point nobody publishes. Licence is MIT, and the LICENSE file sits at the top level next to `setup.py` and `pyproject.toml`.

The C++ API paragraph is in worse shape than the benchmark gap. It states that a C++ equivalent of the python models exists and that you should add `TorchLib` to `-DCMAKE_PREFIX_PATH`, then the text stops mid-instruction at `import torch; pr` with no finished command. The `CMakeLists.txt` and `cmake/` entries are where a C++ consumer has to start reading instead.

Editorial conclusion

Adopt pytorch_scatter when your model reduces neighbour features and you want CPU and GPU kernels, sorted and unsorted paths, and backward passes already written. Do not adopt it if you need a CUDA build on macOS or a version string that tracks your torch install. Before pinning, check your exact PyTorch build against the wheel index and read `__version__` in setup.py. Its four composite functions stay on the unsorted path, so standardising on `scatter_softmax` locks the choice. The repository is not archived and its last push landed on 2026-06-03.

Frequently asked questions

Does pytorch_scatter install CUDA wheels on macOS?

No. The wheel tables for PyTorch 2.9, 2.10, 2.11 and 2.12 mark Linux and Windows across every CUDA tag and macOS in the cpu column only. A Mac user gets the CPU implementation and its backward pass, and the README does not list a CUDA wheel for any of those builds.

What is the difference between scatter and segment_coo in pytorch_scatter?

Both are reduce operations driven by a group index, but segment_coo requires that index to be sorted while scatter accepts arbitrary order. All of the operations reduce with "sum", "mean", "min" or "max".

Why does pip try to compile pytorch_scatter from source?

For older PyTorch builds the README says to name the supported version explicitly or install via pip install --no-index, precisely to prevent a manual installation from source. The version constant in setup.py is still the hard-coded literal 2.1.2 while the wheel index carries PyTorch 2.12 builds.

How do I run the tests for pytorch_scatter?

The README gives a single command, pytest, and the repository has a test/ directory at the top level. It documents no command for the benchmark/ directory.

Does pytorch_scatter have a C++ API?

Yes, the README states that it offers a C++ API containing C++ equivalents of the python models, and that you need to add TorchLib to the -DCMAKE_PREFIX_PATH. The paragraph then cuts off mid-instruction, so the full command is not given, and the top-level CMakeLists.txt and cmake/ directory are where you would look next.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. rusty1s/pytorch_scatter on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/rusty1s-pytorch-scatter.svg)](https://hysenlabs.com/projects/rusty1s-pytorch-scatter)

Was this useful?